From e56a36ae66ee3e4f7c7c68d50ebd100fdf905d03 Mon Sep 17 00:00:00 2001 From: emil <33105890+poolsar42@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:14:18 -0400 Subject: [PATCH] chore(console): refactoring 3DS components --- apps/console/app/root.tsx | 2 +- .../app/routes/__layout/billing/index.tsx | 104 +++------ .../app/routes/apps/$clientId/billing.tsx | 218 +++++++++++------- apps/console/app/services/billing/stripe.ts | 8 +- apps/console/app/utils/stripe.ts | 125 +++++++++- 5 files changed, 297 insertions(+), 160 deletions(-) diff --git a/apps/console/app/root.tsx b/apps/console/app/root.tsx index 09c60614d8..f3781f0654 100644 --- a/apps/console/app/root.tsx +++ b/apps/console/app/root.tsx @@ -146,7 +146,7 @@ export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper( WALLET_CONNECT_PROJECT_ID, } = context.env - const spd = await accountClient.getStripePaymentData.query({ + const spd = await coreClient.account.getStripePaymentData.query({ accountURN, }) diff --git a/apps/console/app/routes/__layout/billing/index.tsx b/apps/console/app/routes/__layout/billing/index.tsx index 6ffd3ea051..fb00896e29 100644 --- a/apps/console/app/routes/__layout/billing/index.tsx +++ b/apps/console/app/routes/__layout/billing/index.tsx @@ -66,20 +66,18 @@ import { import useConnectResult from '@proofzero/design-system/src/hooks/useConnectResult' import { ToastInfo } from '@proofzero/design-system/src/atoms/toast/ToastInfo' import { DangerPill } from '@proofzero/design-system/src/atoms/pills/DangerPill' -import { - createSubscription, - reconcileAppSubscriptions, - updateSubscription, -} from '~/services/billing/stripe' +import { reconcileAppSubscriptions } from '~/services/billing/stripe' import { useHydrated } from 'remix-utils' import _ from 'lodash' import { BadRequestError, InternalServerError } from '@proofzero/errors' import { + createOrUpdateSubscription, getCurrentAndUpcomingInvoices, + process3DSecureCard, + setPurchaseToastNotification, type StripeInvoice, } from '~/utils/stripe' import { IoWarningOutline } from 'react-icons/io5' -import { loadStripe } from '@stripe/stripe-js' import { type ToastNotification } from '~/types' type LoaderData = { @@ -186,7 +184,7 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper( ...traceHeader, }) - const spd = await accountClient.getStripePaymentData.query({ + const spd = await coreClient.account.getStripePaymentData.query({ accountURN, }) @@ -200,7 +198,7 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper( for (const invoice of invoices) { // We are not creating and/or updating subscriptions // until we resolve our unpaid invoices - if (invoice.status) { + if (invoice?.status) { if (['open', 'uncollectible'].includes(invoice.status)) { flashSession.flash( 'toast_notification', @@ -249,29 +247,14 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper( accountURN, }) - let sub - if (!entitlements.subscriptionID) { - sub = await createSubscription( - { - customerID: customerID, - planID: context.env.SECRET_STRIPE_PRO_PLAN_ID, - quantity: +quantity, - accountURN, - handled: true, - }, - context.env - ) - } else { - sub = await updateSubscription( - { - subscriptionID: entitlements.subscriptionID, - planID: context.env.SECRET_STRIPE_PRO_PLAN_ID, - quantity: +quantity, - handled: true, - }, - context.env - ) - } + const sub = await createOrUpdateSubscription({ + customerID, + SECRET_STRIPE_PRO_PLAN_ID: context.env.SECRET_STRIPE_PRO_PLAN_ID, + SECRET_STRIPE_API_KEY: context.env.SECRET_STRIPE_API_KEY, + quantity, + subscriptionID: entitlements.subscriptionID, + accountURN, + }) if ( (txType === 'buy' && @@ -292,23 +275,10 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper( if (txType === 'buy') { // https://stripe.com/docs/billing/subscriptions/overview#subscription-statuses - if (sub.status === 'active' || sub.status === 'trialing') { - flashSession.flash( - 'toast_notification', - JSON.stringify({ - type: ToastType.Success, - message: 'Entitlement(s) successfully bought', - }) - ) - } else { - flashSession.flash( - 'toast_notification', - JSON.stringify({ - type: ToastType.Error, - message: 'Payment failed - check your card details', - }) - ) - } + setPurchaseToastNotification({ + sub, + flashSession, + }) } if (txType === 'remove') { flashSession.flash( @@ -322,13 +292,18 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper( return new Response( JSON.stringify({ - status: (sub.latest_invoice as unknown as StripeInvoice).payment_intent! - .status, + status: (sub.latest_invoice as unknown as StripeInvoice)?.payment_intent + ?.status, client_secret: (sub.latest_invoice as unknown as StripeInvoice) - .payment_intent!.client_secret, + .payment_intent?.client_secret, payment_method: (sub.latest_invoice as unknown as StripeInvoice) - .payment_intent!.payment_method, - }) + .payment_intent?.payment_method, + }), + { + headers: { + 'Set-Cookie': await commitFlashSession(flashSession, context.env), + }, + } ) } ) @@ -1006,29 +981,22 @@ export default () => { useEffect(() => { if (actionData) { ;(async () => { - const stripeClient = await loadStripe(STRIPE_PUBLISHABLE_KEY) - const { status, client_secret, payment_method } = JSON.parse(actionData) - if (status === 'requires_action') { - toast(ToastType.Warning, { - message: 'Payment requires additional action', - }) - await stripeClient?.confirmCardPayment(client_secret, { - payment_method: payment_method, - }) - // Approximately enough for webhook to be called and update entitlements - setTimeout(() => { - navigate('.', { replace: true }) - }, 2000) - } + await process3DSecureCard({ + STRIPE_PUBLISHABLE_KEY, + actionData, + navigate, + }) })() } + }, [actionData]) + useEffect(() => { if (toastNotification) { toast(toastNotification.type, { message: toastNotification.message, }) } - }, [toastNotification, actionData]) + }, [toastNotification]) const redirectToPassport = () => { const currentURL = new URL(window.location.href) diff --git a/apps/console/app/routes/apps/$clientId/billing.tsx b/apps/console/app/routes/apps/$clientId/billing.tsx index e7484cb0eb..8e6ea1d9c4 100644 --- a/apps/console/app/routes/apps/$clientId/billing.tsx +++ b/apps/console/app/routes/apps/$clientId/billing.tsx @@ -23,16 +23,19 @@ import { getAuthzHeaderConditionallyFromToken, parseJwt, } from '@proofzero/utils' -import { useLoaderData, useOutletContext, useSubmit } from '@remix-run/react' +import { + useActionData, + useLoaderData, + useNavigate, + useOutletContext, + useSubmit, +} from '@remix-run/react' import { type GetEntitlementsOutput } from '@proofzero/platform/account/src/jsonrpc/methods/getEntitlements' import { type AccountURN } from '@proofzero/urns/account' import { BadRequestError } from '@proofzero/errors' import type { ToastNotification, appDetailsProps } from '~/types' import { type AppLoaderData } from '~/root' -import { - createSubscription, - updateSubscription, -} from '~/services/billing/stripe' + import { Modal } from '@proofzero/design-system/src/molecules/modal/Modal' import { ToastWithLink } from '@proofzero/design-system/src/atoms/toast/ToastWithLink' import { useEffect, useMemo, useState } from 'react' @@ -43,6 +46,13 @@ import { toast, } from '@proofzero/design-system/src/atoms/toast' import { type Env } from 'bindings' +import { + type StripeInvoice, + getCurrentAndUpcomingInvoices, + setPurchaseToastNotification, + createOrUpdateSubscription, + process3DSecureCard, +} from '~/utils/stripe' export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper( async ({ request, context }) => { @@ -75,6 +85,7 @@ export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper( entitlements, paymentData, toastNotification, + STRIPE_PUBLISHABLE_KEY: context.env.STRIPE_PUBLISHABLE_KEY, }, { headers: { @@ -163,86 +174,41 @@ const processPurchaseOp = async ( const { customerID } = paymentData let sub - let quantity - try { - if (!entitlements.subscriptionID) { - quantity = 1 - sub = await createSubscription( - { - customerID: customerID, - planID: env.SECRET_STRIPE_PRO_PLAN_ID, - quantity, - accountURN, - handled: true, - }, - env - ) - } else { - quantity = entitlements.plans[plan]?.entitlements - ? entitlements.plans[plan]?.entitlements! + 1 - : 1 - - sub = await updateSubscription( - { - subscriptionID: entitlements.subscriptionID, - planID: env.SECRET_STRIPE_PRO_PLAN_ID, - quantity, - handled: true, - }, - env - ) - } - - if (sub.status !== 'active' && sub.status !== 'trialing') { - flashSession.flash( - 'toast_notification', - JSON.stringify({ - type: ToastType.Error, - message: 'Payment failed - check your card details', - }) - ) - return new Response(null, { - headers: { - 'Set-Cookie': await commitFlashSession(flashSession, env), - }, - }) - } - } catch (e) { - flashSession.flash( - 'toast_notification', - JSON.stringify({ - type: ToastType.Error, - message: 'Transaction failed. You were not charged.', - }) - ) - - return new Response(null, { - headers: { - 'Set-Cookie': await commitFlashSession(flashSession, env), - }, - }) - } - - await coreClient.account.updateEntitlements.mutate({ - accountURN: accountURN, - subscriptionID: sub.id, - quantity: quantity, - type: plan, + const quantity = entitlements.subscriptionID + ? entitlements.plans[plan]?.entitlements + ? entitlements.plans[plan]?.entitlements! + 1 + : 1 + : 1 + + sub = await createOrUpdateSubscription({ + customerID, + SECRET_STRIPE_PRO_PLAN_ID: env.SECRET_STRIPE_PRO_PLAN_ID, + SECRET_STRIPE_API_KEY: env.SECRET_STRIPE_API_KEY, + quantity, + subscriptionID: entitlements.subscriptionID, + accountURN, }) - await coreClient.starbase.setAppPlan.mutate({ - accountURN, - clientId, - plan, + setPurchaseToastNotification({ + sub, + flashSession, }) + if (sub.status === 'active' || sub.status === 'trialing') { + await coreClient.account.updateEntitlements.mutate({ + accountURN: accountURN, + subscriptionID: sub.id, + quantity: quantity, + type: plan, + }) - flashSession.flash( - 'toast_notification', - JSON.stringify({ - type: ToastType.Success, - message: `${plans[plan].title} purchased and assigned.`, + await coreClient.starbase.setAppPlan.mutate({ + accountURN, + clientId, + plan, }) - ) + } + + return sub } export const action: ActionFunction = getRollupReqFunctionErrorWrapper( @@ -254,19 +220,57 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper( }) } + const parsedJwt = parseJwt(jwt!) + const accountURN = parsedJwt.sub as AccountURN + const { clientId } = params if (!clientId) throw new BadRequestError({ message: 'Missing Client ID' }) const traceHeader = generateTraceContextHeaders(context.traceSpan) + const coreClient = createCoreClient(context.env.Core, { + ...getAuthzHeaderConditionallyFromToken(jwt), + ...traceHeader, + }) + + const spd = await coreClient.account.getStripePaymentData.query({ + accountURN, + }) + + const invoices = await getCurrentAndUpcomingInvoices( + spd, + context.env.SECRET_STRIPE_API_KEY + ) + + const flashSession = await getFlashSession(request, context.env) + + for (const invoice of invoices) { + // We are not creating and/or updating subscriptions + // until we resolve our unpaid invoices + if (invoice?.status) { + if (['open', 'uncollectible'].includes(invoice.status)) { + flashSession.flash( + 'toast_notification', + JSON.stringify({ + type: ToastType.Error, + message: 'Payment failed - check your card details', + }) + ) + return new Response(null, { + headers: { + 'Set-Cookie': await commitFlashSession(flashSession, context.env), + }, + }) + } + } + } + const fd = await request.formData() const op = fd.get('op') as 'update' | 'purchase' const { plan } = JSON.parse(fd.get('payload') as string) as { plan: ServicePlanType } - const flashSession = await getFlashSession(request, context.env) - switch (op) { case 'update': { await processUpdateOp( @@ -281,7 +285,7 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper( } case 'purchase': { - await processPurchaseOp( + const sub = await processPurchaseOp( jwt, plan, clientId, @@ -289,7 +293,22 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper( context.env, traceHeader ) - break + + return new Response( + JSON.stringify({ + status: (sub?.latest_invoice as unknown as StripeInvoice) + ?.payment_intent?.status, + client_secret: (sub?.latest_invoice as unknown as StripeInvoice) + .payment_intent?.client_secret, + payment_method: (sub?.latest_invoice as unknown as StripeInvoice) + .payment_intent?.payment_method, + }), + { + headers: { + 'Set-Cookie': await commitFlashSession(flashSession, context.env), + }, + } + ) } } @@ -313,12 +332,14 @@ const PlanCard = ({ totalEntitlements, usedEntitlements, paymentData, + hasUnpaidInvoices, }: { planType: ServicePlanType currentPlan: ServicePlanType totalEntitlements?: number usedEntitlements?: number paymentData: PaymentData + hasUnpaidInvoices: boolean }) => { const plan = plans[planType] const active = planType === currentPlan @@ -351,6 +372,7 @@ const PlanCard = ({ {!active && ( void plan: PlanDetails paymentData?: PaymentData + hasUnpaidInvoices: boolean }) => { const submit = useSubmit() @@ -484,7 +508,7 @@ const PurchaseConfirmationModal = ({