From e22ec6ae524126eccc4240cfd487a5a7df98e59f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 30 May 2025 15:51:17 +0200 Subject: [PATCH] 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 --- package.json | 1 + pnpm-lock.yaml | 34 +++++++++++ .../components/billing/paymentModal.svelte | 18 ++++-- src/lib/components/creditCardInfo.svelte | 5 +- src/lib/components/fakeModal.svelte | 2 +- .../filters/filtersBottomSheet.svelte | 2 - src/lib/components/navbar.svelte | 4 ++ src/lib/stores/stripe.ts | 61 +++++++++++++------ src/routes/(console)/account/header.svelte | 2 +- .../account/payments/paymentMethods.svelte | 24 +++++--- .../billing/paymentMethods.svelte | 14 +++-- .../auth/(providers)/googleOAuth.svelte | 4 +- .../auth/(providers)/oidcOAuth.svelte | 5 +- 13 files changed, 130 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 4fda9722f..89b6c5ec4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f9ac752e..30389e998 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/lib/components/billing/paymentModal.svelte b/src/lib/components/billing/paymentModal.svelte index 4f1afcdf6..9ef191311 100644 --- a/src/lib/components/billing/paymentModal.svelte +++ b/src/lib/components/billing/paymentModal.svelte @@ -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 @@ } - + {/if} -
+
@@ -105,7 +114,8 @@ .loader-element { width: 100%; align-self: center; - justify-items: end; + justify-content: center; + display: flex; } } diff --git a/src/lib/components/creditCardInfo.svelte b/src/lib/components/creditCardInfo.svelte index 849146456..f750e87c3 100644 --- a/src/lib/components/creditCardInfo.svelte +++ b/src/lib/components/creditCardInfo.svelte @@ -19,8 +19,9 @@ {paymentMethod?.name} -{paymentMethod?.expiryMonth}/{paymentMethod?.expiryYear} + + {paymentMethod?.expiryMonth}/{paymentMethod?.expiryYear} + {#if paymentMethod?.lastError || paymentMethod?.expired} diff --git a/src/lib/components/fakeModal.svelte b/src/lib/components/fakeModal.svelte index 0db0ad65d..13a40cb2d 100644 --- a/src/lib/components/fakeModal.svelte +++ b/src/lib/components/fakeModal.svelte @@ -30,7 +30,7 @@ } } - function closeModal() { + export function closeModal() { document.documentElement.classList.remove('u-overflow-hidden'); show = false; } diff --git a/src/lib/components/filters/filtersBottomSheet.svelte b/src/lib/components/filters/filtersBottomSheet.svelte index 5d7137c1a..ac3e0a1d4 100644 --- a/src/lib/components/filters/filtersBottomSheet.svelte +++ b/src/lib/components/filters/filtersBottomSheet.svelte @@ -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 }; diff --git a/src/lib/components/navbar.svelte b/src/lib/components/navbar.svelte index 02ac75d28..31f3378f2 100644 --- a/src/lib/components/navbar.svelte +++ b/src/lib/components/navbar.svelte @@ -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; + }); diff --git a/src/lib/stores/stripe.ts b/src/lib/stores/stripe.ts index 86651a3e8..6973bcb04 100644 --- a/src/lib/stores/stripe.ts +++ b/src/lib/stores/stripe.ts @@ -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(); 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)', diff --git a/src/routes/(console)/account/header.svelte b/src/routes/(console)/account/header.svelte index b0f030d80..507dbfd91 100644 --- a/src/routes/(console)/account/header.svelte +++ b/src/routes/(console)/account/header.svelte @@ -41,7 +41,7 @@ ...permanentTabs, { href: `${path}/payments`, - title: 'Payment details', + title: 'Payments', event: 'payments', hasChildren: true } diff --git a/src/routes/(console)/account/payments/paymentMethods.svelte b/src/routes/(console)/account/payments/paymentMethods.svelte index 6b700d4c2..de20933c9 100644 --- a/src/routes/(console)/account/payments/paymentMethods.svelte +++ b/src/routes/(console)/account/payments/paymentMethods.svelte @@ -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); @@ -50,18 +57,21 @@ Credit card Name - Expiration date + + Expiration date + + + {#each filteredMethods as paymentMethod, i} diff --git a/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte b/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte index 6cb9717e2..4f0d7e131 100644 --- a/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte +++ b/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte @@ -99,6 +99,12 @@ $: if (!showReplace) { isSelectedBackup = false; } + + $: hasPaymentError = + defaultPaymentMethod?.lastError || + defaultPaymentMethod.expired || + backupPaymentMethod?.lastError || + backupPaymentMethod.expired; @@ -109,10 +115,10 @@ diff --git a/src/routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte b/src/routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte index 82109ee1b..ee90d2bb7 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte @@ -39,10 +39,10 @@ }; - + {provider.name} OAuth2 settings -

+

To use {provider.name} authentication in your application, first fill in this form. For more info you can - - {provider.name} OAuth2 settings -

+ +

To use {provider.name} authentication in your application, first fill in this form. For more info you can