diff --git a/package-lock.json b/package-lock.json index 860b75e0ab..68578b62ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11605,6 +11605,20 @@ "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, + "node_modules/@stripe/react-stripe-js": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.9.0.tgz", + "integrity": "sha512-pN1Re7zUc3m61FFQROok685g3zsBQRzCmZDmTzO8iPU6zhLvu2JnC0LrG0FCzSp6kgGa8AQSzq4rpFSgyhkjKg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": ">=1.44.1 <8.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, "node_modules/@stripe/stripe-js": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.7.0.tgz", @@ -44655,6 +44669,7 @@ "@overleaf/stream-utils": "*", "@phosphor-icons/react": "^2.1.7", "@slack/webhook": "^7.0.2", + "@stripe/react-stripe-js": "^3.9.0", "@stripe/stripe-js": "^7.7.0", "@xmldom/xmldom": "^0.7.13", "accepts": "^1.3.7", diff --git a/services/web/frontend/js/shared/utils/currency.ts b/services/web/frontend/js/shared/utils/currency.ts index 5a51150d56..0d1a326642 100644 --- a/services/web/frontend/js/shared/utils/currency.ts +++ b/services/web/frontend/js/shared/utils/currency.ts @@ -26,3 +26,19 @@ export function formatCurrency( return `${currency} ${amount}` } + +export function convertToMinorUnits(amount: number, currency: string): number { + const isNoCentsCurrency = ['clp', 'jpy', 'krw', 'vnd'].includes( + currency.toLowerCase() + ) + + // Determine the multiplier based on currency + let multiplier = 100 // default for most currencies (2 decimal places) + + if (isNoCentsCurrency) { + multiplier = 1 // no decimal places + } + + // Convert and round to an integer + return Math.round(amount * multiplier) +} diff --git a/services/web/frontend/js/utils/meta.ts b/services/web/frontend/js/utils/meta.ts index 182fe604b6..16d5191977 100644 --- a/services/web/frontend/js/utils/meta.ts +++ b/services/web/frontend/js/utils/meta.ts @@ -57,6 +57,7 @@ import { Subscription as ProjectDashboardSubscription } from '../../../types/pro import { ThirdPartyIds } from '../../../types/third-party-ids' import { Publisher } from '../../../types/subscription/dashboard/publisher' import { SubscriptionChangePreview } from '../../../types/subscription/subscription-change-preview' +import { SubscriptionCreationPreview } from '../../../types/subscription/subscription-creation-preview' import { DefaultNavbarMetadata } from '@/shared/components/types/default-navbar-metadata' import { FooterMetadata } from '@/shared/components/types/footer-metadata' import type { ScriptLogType } from '../../../modules/admin-panel/frontend/js/features/script-logs/script-log' @@ -270,6 +271,8 @@ export interface Meta { 'ol-ssoErrorMessage': string 'ol-ssoInitPath': string 'ol-stripeAccountId': string + 'ol-stripePublicKeyUK': string + 'ol-stripePublicKeyUS': string 'ol-stripeSubscriptionData': { customerId: string subscriptionState: string | null @@ -277,6 +280,7 @@ export interface Meta { } 'ol-subscription': any // TODO: mixed types, split into two fields 'ol-subscriptionChangePreview': SubscriptionChangePreview + 'ol-subscriptionCreationPreview': SubscriptionCreationPreview 'ol-subscriptionFeatures': { managedUsers?: boolean groupSSO?: boolean diff --git a/services/web/package.json b/services/web/package.json index c2cbdca8be..3d52678639 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -94,6 +94,7 @@ "@overleaf/stream-utils": "*", "@phosphor-icons/react": "^2.1.7", "@slack/webhook": "^7.0.2", + "@stripe/react-stripe-js": "^3.9.0", "@stripe/stripe-js": "^7.7.0", "@xmldom/xmldom": "^0.7.13", "accepts": "^1.3.7", diff --git a/services/web/types/subscription/dashboard/subscription.ts b/services/web/types/subscription/dashboard/subscription.ts index ff18945bc7..8ff63b2342 100644 --- a/services/web/types/subscription/dashboard/subscription.ts +++ b/services/web/types/subscription/dashboard/subscription.ts @@ -20,10 +20,12 @@ export type PurchasingAddOnCode = { code: string } -type PaymentProviderCoupon = { +export type PaymentProviderCoupon = { code: string name: string - description: string + description?: string + isSingleUse?: boolean + discountMonths?: number | null } type PaymentProviderRecord = { diff --git a/services/web/types/subscription/subscription-change-preview.ts b/services/web/types/subscription/subscription-change-preview.ts index 463c39ade4..4821b650e7 100644 --- a/services/web/types/subscription/subscription-change-preview.ts +++ b/services/web/types/subscription/subscription-change-preview.ts @@ -1,3 +1,18 @@ +export type ImmediateCharge = { + subtotal: number + tax: number + total: number + discount: number + lineItems: { + planCode: string | null | undefined + description: string + subtotal: number + discount: number + tax: number + isAiAssist: boolean + }[] +} + export type SubscriptionChangePreview = { change: SubscriptionChangeDescription currency: string @@ -6,20 +21,7 @@ export type SubscriptionChangePreview = { nextPlan: { annual: boolean } - immediateCharge: { - subtotal: number - tax: number - total: number - discount: number - lineItems: { - planCode: string | null | undefined - description: string - subtotal: number - discount: number - tax: number - isAiAssist: boolean - }[] - } + immediateCharge: ImmediateCharge nextInvoice: { date: string plan: { diff --git a/services/web/types/subscription/subscription-creation-preview.ts b/services/web/types/subscription/subscription-creation-preview.ts new file mode 100644 index 0000000000..3588335c0b --- /dev/null +++ b/services/web/types/subscription/subscription-creation-preview.ts @@ -0,0 +1,12 @@ +import { ImmediateCharge } from './subscription-change-preview' +import { PaymentProviderCoupon } from './dashboard/subscription' +import { Plan } from './plan' + +export type SubscriptionCreationPreview = { + immediateCharge: ImmediateCharge + taxRate: number + billingCycleInterval: 'month' | 'year' + coupon: PaymentProviderCoupon + trialLength: number | null + plan: Plan +}