mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Fix various issues in billing flows
- Add color package for color manipulation - Update Stripe form appearance for light and dark themes with proper color conversion and improved focus states - Fix payment modal loading state display - Improve payment method table column widths and conditional visibility - Fix navbar account menu closing on navigation - Update OAuth modal descriptions and remove duplicate titles - Export closeModal method from FakeModal component
This commit is contained in:
@@ -65,6 +65,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.28.0",
|
||||
"@typescript-eslint/parser": "^8.28.0",
|
||||
"@vitest/ui": "^3.0.9",
|
||||
"color": "^5.0.0",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-config-prettier": "^10.1.0",
|
||||
"eslint-plugin-svelte": "^3.3.3",
|
||||
|
||||
Generated
+34
@@ -135,6 +135,9 @@ importers:
|
||||
'@vitest/ui':
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9(vitest@3.0.9)
|
||||
color:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
eslint:
|
||||
specifier: ^9.23.0
|
||||
version: 9.23.0
|
||||
@@ -1714,9 +1717,25 @@ packages:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-convert@3.1.0:
|
||||
resolution: {integrity: sha512-TVoqAq8ZDIpK5lsQY874DDnu65CSsc9vzq0wLpNQ6UMBq81GSZocVazPiBbYGzngzBOIRahpkTzCLVe2at4MfA==}
|
||||
engines: {node: '>=14.6'}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
color-name@2.0.0:
|
||||
resolution: {integrity: sha512-SbtvAMWvASO5TE2QP07jHBMXKafgdZz8Vrsrn96fiL+O92/FN/PLARzUW5sKt013fjAprK2d2iCn2hk2Xb5oow==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
color-string@2.0.1:
|
||||
resolution: {integrity: sha512-5z9FbYTZPAo8iKsNEqRNv+OlpBbDcoE+SY9GjLfDUHEfcNNV7tS9eSAlFHEaub/r5tBL9LtskAeq1l9SaoZ5tQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
color@5.0.0:
|
||||
resolution: {integrity: sha512-16BlyiuyLq3MLxpRWyOTiWsO3ii/eLQLJUQXBSNcxMBBSnyt1ee9YUdaozQp03ifwm5woztEZGDbk9RGVuCsdw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -5256,8 +5275,23 @@ snapshots:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
color-convert@3.1.0:
|
||||
dependencies:
|
||||
color-name: 2.0.0
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
color-name@2.0.0: {}
|
||||
|
||||
color-string@2.0.1:
|
||||
dependencies:
|
||||
color-name: 2.0.0
|
||||
|
||||
color@5.0.0:
|
||||
dependencies:
|
||||
color-convert: 3.1.0
|
||||
color-string: 2.0.1
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
|
||||
let name: string;
|
||||
let error: string;
|
||||
let modal: FakeModal;
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const card = await submitStripeCard(name, page?.params?.organization ?? null);
|
||||
show = false;
|
||||
modal.closeModal();
|
||||
invalidate(Dependencies.PAYMENT_METHODS);
|
||||
dispatch('submit', card);
|
||||
addNotification({
|
||||
@@ -65,7 +66,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FakeModal bind:show title="Add payment method" bind:error onSubmit={handleSubmit}>
|
||||
<FakeModal
|
||||
bind:this={modal}
|
||||
bind:show
|
||||
title="Add payment method"
|
||||
bind:error
|
||||
onSubmit={handleSubmit}>
|
||||
<slot />
|
||||
<InputText
|
||||
id="name"
|
||||
@@ -82,7 +88,10 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="stripe-element" bind:this={element}>
|
||||
<div
|
||||
style:display={isLoading ? 'none' : 'initial'}
|
||||
class="stripe-element"
|
||||
bind:this={element}>
|
||||
<!-- Stripe will create form elements here -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,7 +114,8 @@
|
||||
.loader-element {
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
justify-items: end;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="name" {root}>{paymentMethod?.name}</Table.Cell>
|
||||
<Table.Cell column="expiry" {root}
|
||||
>{paymentMethod?.expiryMonth}/{paymentMethod?.expiryYear}</Table.Cell>
|
||||
<Table.Cell column="expiry" {root}>
|
||||
{paymentMethod?.expiryMonth}/{paymentMethod?.expiryYear}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="status" {root}>
|
||||
{#if paymentMethod?.lastError || paymentMethod?.expired}
|
||||
<Popover let:toggle>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
export function closeModal() {
|
||||
document.documentElement.classList.remove('u-overflow-hidden');
|
||||
show = false;
|
||||
}
|
||||
|
||||
@@ -65,8 +65,6 @@
|
||||
items: filterCols.map((col) => {
|
||||
return {
|
||||
name: col.title,
|
||||
onClick: () =>
|
||||
console.log(subSheets.find((sheet) => sheet?.title === col?.title)),
|
||||
subMenu: subSheets.find((sheet) => sheet?.title === col?.title),
|
||||
trailingIcon: IconChevronRight
|
||||
};
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
import { user } from '$lib/stores/user';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
|
||||
let showSupport = false;
|
||||
|
||||
@@ -115,6 +116,9 @@
|
||||
|
||||
$: currentOrg = organizations.find((org) => org.isSelected);
|
||||
$: selectedProject = currentOrg?.projects.find((project) => project.isSelected);
|
||||
beforeNavigate(() => {
|
||||
showAccountMenu = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<Navbar.Base {...$$props}>
|
||||
|
||||
+41
-20
@@ -7,7 +7,8 @@ import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from './notifications';
|
||||
import { organization } from './organization';
|
||||
import { base } from '$app/paths';
|
||||
import { ThemeLightCloud } from '$themes';
|
||||
import { ThemeDarkCloud, ThemeLightCloud } from '$themes';
|
||||
import Color from 'color';
|
||||
|
||||
export const stripe = writable<Stripe>();
|
||||
let paymentMethod: PaymentMethodData;
|
||||
@@ -185,37 +186,44 @@ export async function confirmSetup(
|
||||
}
|
||||
}
|
||||
|
||||
function toRGB(color: string): string {
|
||||
return Color(color).rgb().string();
|
||||
}
|
||||
|
||||
const appearanceLight: Appearance = {
|
||||
variables: {
|
||||
fontSizeBase: ThemeLightCloud['font-size-s'],
|
||||
fontSizeSm: ThemeLightCloud['font-size-s'],
|
||||
colorPrimary: ThemeLightCloud['neutral-700'],
|
||||
colorText: ThemeLightCloud['neutral-700'],
|
||||
colorBackground: 'rgb(250, 250, 251)',
|
||||
colorDanger: ThemeLightCloud['fgcolor-error'],
|
||||
colorPrimary: toRGB(ThemeLightCloud['neutral-700']),
|
||||
colorTextSecondary: toRGB(ThemeLightCloud['neutral-700']),
|
||||
colorText: toRGB(ThemeLightCloud['neutral-700']),
|
||||
colorBackground: toRGB(ThemeLightCloud['neutral-25']),
|
||||
colorDanger: toRGB(ThemeLightCloud['web-red-700']),
|
||||
fontFamily: ThemeLightCloud['font-family-sansserif'],
|
||||
borderRadius: ThemeLightCloud['base-8']
|
||||
},
|
||||
rules: {
|
||||
'.Label': {
|
||||
color: ThemeLightCloud['neutral-700'],
|
||||
color: toRGB(ThemeLightCloud['neutral-700']),
|
||||
marginBottom: ThemeLightCloud['base-8'],
|
||||
fontWeight: '500'
|
||||
},
|
||||
'.Input': {
|
||||
padding: ThemeLightCloud['base-6'],
|
||||
paddingLeft: ThemeLightCloud['base-12']
|
||||
paddingLeft: ThemeLightCloud['base-12'],
|
||||
outlineOffset: '-1px'
|
||||
},
|
||||
'.Input:hover': {
|
||||
border: 'solid 1px rgb(195, 195, 198)',
|
||||
border: '1px solid ' + toRGB(ThemeLightCloud['neutral-250']),
|
||||
boxShadow: 'none'
|
||||
},
|
||||
'.Input:focus': {
|
||||
border: 'solid 1px rgb(195, 195, 198)',
|
||||
border: '1px solid ' + toRGB(ThemeLightCloud['neutral-250']),
|
||||
outline: '2px solid ' + toRGB(ThemeLightCloud['neutral-250']),
|
||||
boxShadow: 'none'
|
||||
},
|
||||
'.Input::placeholder': {
|
||||
color: '#C4C6D7'
|
||||
color: toRGB(ThemeLightCloud['neutral-250'])
|
||||
},
|
||||
'.Input--invalid': {
|
||||
border: 'solid 1px var(--colorDanger)',
|
||||
@@ -226,25 +234,38 @@ const appearanceLight: Appearance = {
|
||||
|
||||
const appearanceDark = {
|
||||
variables: {
|
||||
colorPrimary: '#606a7b',
|
||||
colorText: 'rgb(195, 195, 198)',
|
||||
colorBackground: 'rgb(24, 24, 27)',
|
||||
colorDanger: '#FF453A',
|
||||
fontFamily: 'Inter, arial, sans-serif',
|
||||
borderRadius: '4px',
|
||||
spacingGridRow: '16px'
|
||||
fontSizeBase: ThemeDarkCloud['font-size-s'],
|
||||
fontSizeSm: ThemeDarkCloud['font-size-s'],
|
||||
colorPrimary: toRGB(ThemeDarkCloud['neutral-250']),
|
||||
colorText: toRGB(ThemeDarkCloud['neutral-250']),
|
||||
colorTextSecondary: toRGB(ThemeDarkCloud['neutral-250']),
|
||||
colorBackground: toRGB(ThemeDarkCloud['neutral-900']),
|
||||
colorDanger: toRGB(ThemeDarkCloud['web-red-500']),
|
||||
fontFamily: ThemeDarkCloud['font-family-sansserif'],
|
||||
borderRadius: ThemeDarkCloud['base-8']
|
||||
},
|
||||
rules: {
|
||||
'.Label': {
|
||||
color: toRGB(ThemeDarkCloud['neutral-250']),
|
||||
marginBottom: ThemeDarkCloud['base-8'],
|
||||
fontWeight: '500'
|
||||
},
|
||||
'.Input': {
|
||||
padding: ThemeDarkCloud['base-6'],
|
||||
paddingLeft: ThemeDarkCloud['base-12'],
|
||||
outlineOffset: '-1px'
|
||||
},
|
||||
'.Input:hover': {
|
||||
border: 'solid 1px rgb(87, 87, 92)',
|
||||
border: '1px solid ' + toRGB(ThemeDarkCloud['neutral-500']),
|
||||
boxShadow: 'none'
|
||||
},
|
||||
'.Input:focus': {
|
||||
border: 'solid 1px rgb(87, 87, 92)',
|
||||
border: '1px solid ' + toRGB(ThemeDarkCloud['neutral-500']),
|
||||
outline: '2px solid ' + toRGB(ThemeDarkCloud['neutral-500']),
|
||||
boxShadow: 'none'
|
||||
},
|
||||
'.Input::placeholder': {
|
||||
color: 'rgb(87, 87, 92)'
|
||||
color: toRGB(ThemeDarkCloud['neutral-500'])
|
||||
},
|
||||
'.Input--invalid': {
|
||||
border: 'solid 1px var(--colorDanger)',
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
...permanentTabs,
|
||||
{
|
||||
href: `${path}/payments`,
|
||||
title: 'Payment details',
|
||||
title: 'Payments',
|
||||
event: 'payments',
|
||||
hasChildren: true
|
||||
}
|
||||
|
||||
@@ -40,6 +40,13 @@
|
||||
$: filteredMethods = $paymentMethods?.paymentMethods.filter(
|
||||
(method: PaymentMethodData) => !!method?.last4
|
||||
);
|
||||
|
||||
$: hasLinkedOrgs = filteredMethods.some((method) =>
|
||||
orgList.some(
|
||||
(org) => method.$id === org.paymentMethodId || method.$id === org.backupPaymentMethodId
|
||||
)
|
||||
);
|
||||
$: hasPaymentError = filteredMethods.some((method) => method?.lastError || method?.expired);
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
@@ -50,18 +57,21 @@
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
{ id: 'cc' },
|
||||
{ id: 'name' },
|
||||
{ id: 'expiry' },
|
||||
{ id: 'status' },
|
||||
{ id: 'links' },
|
||||
{ id: 'cc', width: 140 },
|
||||
{ id: 'name', width: { min: 140 } },
|
||||
{ id: 'expiry', width: 100 },
|
||||
{ id: 'status', width: 110, hide: !hasPaymentError },
|
||||
{ id: 'links', width: 190, hide: !hasLinkedOrgs },
|
||||
{ id: 'actions', width: 40 }
|
||||
]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="cc" {root}>Credit card</Table.Header.Cell>
|
||||
<Table.Header.Cell column="name" {root}>Name</Table.Header.Cell>
|
||||
<Table.Header.Cell column="expiration" {root}
|
||||
>Expiration date</Table.Header.Cell>
|
||||
<Table.Header.Cell column="expiration" {root}>
|
||||
Expiration date
|
||||
</Table.Header.Cell>
|
||||
<Table.Header.Cell column="status" {root} />
|
||||
<Table.Header.Cell column="links" {root} />
|
||||
<Table.Header.Cell column="actions" {root} />
|
||||
</svelte:fragment>
|
||||
{#each filteredMethods as paymentMethod, i}
|
||||
|
||||
@@ -99,6 +99,12 @@
|
||||
$: if (!showReplace) {
|
||||
isSelectedBackup = false;
|
||||
}
|
||||
|
||||
$: hasPaymentError =
|
||||
defaultPaymentMethod?.lastError ||
|
||||
defaultPaymentMethod.expired ||
|
||||
backupPaymentMethod?.lastError ||
|
||||
backupPaymentMethod.expired;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
@@ -109,10 +115,10 @@
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
{ id: 'cc' },
|
||||
{ id: 'name' },
|
||||
{ id: 'expiry' },
|
||||
{ id: 'status' },
|
||||
{ id: 'cc', width: 140 },
|
||||
{ id: 'name', width: { min: 140 } },
|
||||
{ id: 'expiry', width: 100 },
|
||||
{ id: 'status', width: 110, hide: !hasPaymentError },
|
||||
{ id: 'actions', width: 40 }
|
||||
]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal {error} size="l" bind:show onSubmit={update} on:close>
|
||||
<Modal {error} bind:show onSubmit={update} title="{provider.name} OAuth2 settings" on:close>
|
||||
<svelte:fragment slot="title">{provider.name} OAuth2 settings</svelte:fragment>
|
||||
|
||||
<p>
|
||||
<p slot="description">
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
|
||||
@@ -65,9 +65,8 @@
|
||||
: provider.secret;
|
||||
</script>
|
||||
|
||||
<Modal {error} onSubmit={update} size="l" bind:show on:close>
|
||||
<svelte:fragment slot="title">{provider.name} OAuth2 settings</svelte:fragment>
|
||||
<p>
|
||||
<Modal {error} onSubmit={update} title="{provider.name} OAuth2 settings" bind:show on:close>
|
||||
<p slot="description">
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={oAuthProvider?.docs} target="_blank" rel="noopener noreferrer"
|
||||
|
||||
Reference in New Issue
Block a user