From d81d5156caf8feb9379787245d17cc39e641f5a7 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 2 Feb 2024 09:31:50 +0530 Subject: [PATCH 1/8] chore: update proto version --- sdks/js/packages/core/client/V1Beta1.ts | 56 ++++++++++++++++++- .../js/packages/core/client/data-contracts.ts | 40 +++++++++++-- 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/sdks/js/packages/core/client/V1Beta1.ts b/sdks/js/packages/core/client/V1Beta1.ts index 1a920f537b..dbee4c5df4 100644 --- a/sdks/js/packages/core/client/V1Beta1.ts +++ b/sdks/js/packages/core/client/V1Beta1.ts @@ -27,12 +27,14 @@ import { V1Beta1BatchCheckPermissionResponse, V1Beta1BillingAccountRequestBody, V1Beta1CancelSubscriptionResponse, + V1Beta1ChangeSubscriptionResponse, V1Beta1CheckFeatureEntitlementResponse, V1Beta1CheckFederatedResourcePermissionRequest, V1Beta1CheckFederatedResourcePermissionResponse, V1Beta1CheckResourcePermissionRequest, V1Beta1CheckResourcePermissionResponse, V1Beta1CheckoutProductBody, + V1Beta1CheckoutSetupBody, V1Beta1CheckoutSubscriptionBody, V1Beta1CreateBillingAccountResponse, V1Beta1CreateBillingUsageResponse, @@ -1557,6 +1559,8 @@ export class V1Beta1 extends HttpClient @@ -1612,10 +1616,20 @@ export class V1Beta1 extends HttpClient + frontierServiceListSubscriptions = ( + orgId: string, + billingId: string, + query?: { + /** Filter subscriptions by state */ + state?: string; + plan?: string; + }, + params: RequestParams = {} + ) => this.request({ path: `/v1beta1/organizations/${orgId}/billing/${billingId}/subscriptions`, method: 'GET', + query: query, secure: true, format: 'json', ...params @@ -1673,14 +1687,52 @@ export class V1Beta1 extends HttpClient + frontierServiceCancelSubscription = ( + orgId: string, + billingId: string, + id: string, + query?: { + immediate?: boolean; + }, + params: RequestParams = {} + ) => this.request({ path: `/v1beta1/organizations/${orgId}/billing/${billingId}/subscriptions/${id}/cancel`, method: 'POST', + query: query, secure: true, format: 'json', ...params }); + /** + * @description Change a subscription plan by ID. + * + * @tags Subscription + * @name FrontierServiceChangeSubscription + * @summary Change subscription plan + * @request POST:/v1beta1/organizations/{org_id}/billing/{billing_id}/subscriptions/{id}/change + * @secure + */ + frontierServiceChangeSubscription = ( + orgId: string, + billingId: string, + id: string, + body: { + /** plan to change to */ + plan?: string; + immediate?: boolean; + }, + params: RequestParams = {} + ) => + this.request({ + path: `/v1beta1/organizations/${orgId}/billing/${billingId}/subscriptions/${id}/change`, + method: 'POST', + body: body, + secure: true, + type: ContentType.Json, + format: 'json', + ...params + }); /** * @description List all transactions of a billing account. * diff --git a/sdks/js/packages/core/client/data-contracts.ts b/sdks/js/packages/core/client/data-contracts.ts index 75ab75ca4d..f8eb990e22 100644 --- a/sdks/js/packages/core/client/data-contracts.ts +++ b/sdks/js/packages/core/client/data-contracts.ts @@ -26,6 +26,19 @@ export interface BillingAccountBalance { updated_at?: string; } +export interface ProductBehaviorConfig { + /** @format int64 */ + credit_amount?: string; + /** @format int64 */ + seat_limit?: string; +} + +export interface SubscriptionPhase { + /** @format date-time */ + effective_at?: string; + plan_id?: string; +} + export interface ProtobufAny { '@type'?: string; [key: string]: any; @@ -219,6 +232,10 @@ export interface V1Beta1BillingTransaction { export type V1Beta1CancelSubscriptionResponse = object; +export interface V1Beta1ChangeSubscriptionResponse { + phase?: SubscriptionPhase; +} + export interface V1Beta1CheckFeatureEntitlementResponse { status?: boolean; } @@ -261,6 +278,8 @@ export interface V1Beta1CheckoutSession { success_url?: string; cancel_url?: string; state?: string; + plan?: string; + product?: string; metadata?: object; /** @format date-time */ created_at?: string; @@ -270,6 +289,10 @@ export interface V1Beta1CheckoutSession { expire_at?: string; } +export interface V1Beta1CheckoutSetupBody { + payment_method?: boolean; +} + export interface V1Beta1CheckoutSubscriptionBody { plan?: string; /** @format int32 */ @@ -417,8 +440,10 @@ export interface V1Beta1CreateUserResponse { } export interface V1Beta1DelegatedCheckoutResponse { - /** Checkout session */ - checkout_session?: V1Beta1CheckoutSession; + /** subscription if created */ + subscription?: V1Beta1Subscription; + /** product if bought */ + product?: V1Beta1Product; } export type V1Beta1DeleteBillingAccountResponse = object; @@ -1204,6 +1229,8 @@ export interface V1Beta1Plan { products?: V1Beta1Product[]; /** known intervals are "day", "week", "month", and "year" */ interval?: string; + /** @format int64 */ + on_start_credits?: string; metadata?: object; /** @format date-time */ created_at?: string; @@ -1218,6 +1245,8 @@ export interface V1Beta1PlanRequestBody { products?: V1Beta1Product[]; /** known intervals are "day", "week", "month", and "year" */ interval?: string; + /** @format int64 */ + on_start_credits?: string; metadata?: object; } @@ -1341,10 +1370,9 @@ export interface V1Beta1Product { plan_ids?: string[]; state?: string; prices?: V1Beta1Price[]; - /** @format int64 */ - credit_amount?: string; behavior?: string; features?: V1Beta1Feature[]; + behavior_config?: ProductBehaviorConfig; metadata?: object; /** @format date-time */ created_at?: string; @@ -1358,10 +1386,9 @@ export interface V1Beta1ProductRequestBody { description?: string; plan_id?: string; prices?: V1Beta1Price[]; - /** @format int64 */ - credit_amount?: string; behavior?: string; features?: V1Beta1Feature[]; + behavior_config?: ProductBehaviorConfig; metadata?: object; } @@ -1586,6 +1613,7 @@ export interface V1Beta1Subscription { canceled_at?: string; /** @format date-time */ ended_at?: string; + phases?: SubscriptionPhase[]; } export interface V1Beta1UpdateBillingAccountResponse { From b8b83cc3d9f6291e608896e49c0dde849cbea22f Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 2 Feb 2024 09:42:08 +0530 Subject: [PATCH 2/8] fix: check empty invoice in billing cycle --- .../organization/billing/upcoming-billing-cycle.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdks/js/packages/core/react/components/organization/billing/upcoming-billing-cycle.tsx b/sdks/js/packages/core/react/components/organization/billing/upcoming-billing-cycle.tsx index cedd12e9c2..b1b7c2610f 100644 --- a/sdks/js/packages/core/react/components/organization/billing/upcoming-billing-cycle.tsx +++ b/sdks/js/packages/core/react/components/organization/billing/upcoming-billing-cycle.tsx @@ -76,7 +76,7 @@ export const UpcomingBillingCycle = () => { billingId ); const invoice = resp?.data?.invoice; - if (invoice) { + if (invoice && invoice.state) { setUpcomingInvoice(invoice); } } catch (err: any) { @@ -184,8 +184,10 @@ export const UpcomingBillingCycle = () => { label="Next billing" value={dayjs(upcomingInvoice?.due_date).format(config.dateFormat)} /> + {/* @ts-ignore */} line + {/* @ts-ignore */} line Date: Fri, 2 Feb 2024 09:42:23 +0530 Subject: [PATCH 3/8] fix: show Upgrade if there is no plan --- sdks/js/packages/core/react/utils/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdks/js/packages/core/react/utils/index.ts b/sdks/js/packages/core/react/utils/index.ts index dde964e23d..981ac4b9a7 100644 --- a/sdks/js/packages/core/react/utils/index.ts +++ b/sdks/js/packages/core/react/utils/index.ts @@ -2,6 +2,7 @@ import dayjs from 'dayjs'; import { V1Beta1Subscription, BillingAccountAddress } from '~/src'; import { IntervalPricingWithPlan } from '~/src/types'; import { SUBSCRIPTION_STATES } from './constants'; +import * as _ from 'lodash'; export const AuthTooltipMessage = 'You don’t have access to perform this action'; @@ -33,7 +34,7 @@ export const getPlanChangeAction = ( currentPlan?: IntervalPricingWithPlan ) => { const diff = nextPlan.weightage - (currentPlan?.weightage || 0); - if (diff > 0) { + if (diff > 0 || _.isEmpty(currentPlan)) { return { btnLabel: 'Upgrade', btnLoadingLabel: 'Upgrading' From be514c1799e55d3d0a3d19d6d8023e4265c5f897 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 2 Feb 2024 14:31:06 +0530 Subject: [PATCH 4/8] chore: add usePlansHook --- .../organization/plans/hooks/usePlans.tsx | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 sdks/js/packages/core/react/components/organization/plans/hooks/usePlans.tsx diff --git a/sdks/js/packages/core/react/components/organization/plans/hooks/usePlans.tsx b/sdks/js/packages/core/react/components/organization/plans/hooks/usePlans.tsx new file mode 100644 index 0000000000..d57c94582e --- /dev/null +++ b/sdks/js/packages/core/react/components/organization/plans/hooks/usePlans.tsx @@ -0,0 +1,72 @@ +import { useCallback, useState } from 'react'; +import { useFrontier } from '~/react/contexts/FrontierContext'; +import qs from 'query-string'; +import { toast } from 'sonner'; +import { V1Beta1CheckoutSession } from '~/src'; + +interface usePlansProps { + onSuccess: (data: V1Beta1CheckoutSession) => void; +} + +export const usePlans = ({ onSuccess = () => {} }: usePlansProps) => { + const [isLoading, setIsLoading] = useState(false); + const { client, activeOrganization, billingAccount, config } = useFrontier(); + + const checkoutPlan = useCallback( + async (planId: string) => { + setIsLoading(true); + try { + if (activeOrganization?.id && billingAccount?.id) { + const query = qs.stringify( + { + details: btoa( + qs.stringify({ + billing_id: billingAccount?.id, + organization_id: activeOrganization?.id, + type: 'plans' + }) + ), + checkout_id: '{{.CheckoutID}}' + }, + { encode: false } + ); + const cancel_url = `${config?.billing?.cancelUrl}?${query}`; + const success_url = `${config?.billing?.successUrl}?${query}`; + + const resp = await client?.frontierServiceCreateCheckout( + activeOrganization?.id, + billingAccount?.id, + { + cancel_url: cancel_url, + success_url: success_url, + subscription_body: { + plan: planId + } + } + ); + if (resp?.data?.checkout_session?.checkout_url) { + onSuccess(resp?.data?.checkout_session); + window.location.href = resp?.data?.checkout_session?.checkout_url; + } + } + } catch (err: any) { + console.error(err); + toast.error('Something went wrong', { + description: err?.message + }); + } finally { + setIsLoading(false); + } + }, + [ + activeOrganization?.id, + billingAccount?.id, + config?.billing?.cancelUrl, + config?.billing?.successUrl, + client, + onSuccess + ] + ); + + return { checkoutPlan, isLoading }; +}; From 4d444a4f359d99f604722e0bec5e440de56ee6f0 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 2 Feb 2024 14:31:36 +0530 Subject: [PATCH 5/8] chore: add usePlanHook --- .../core/react/components/organization/plans/hooks/usePlans.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/sdks/js/packages/core/react/components/organization/plans/hooks/usePlans.tsx b/sdks/js/packages/core/react/components/organization/plans/hooks/usePlans.tsx index d57c94582e..7a54d2e733 100644 --- a/sdks/js/packages/core/react/components/organization/plans/hooks/usePlans.tsx +++ b/sdks/js/packages/core/react/components/organization/plans/hooks/usePlans.tsx @@ -46,7 +46,6 @@ export const usePlans = ({ onSuccess = () => {} }: usePlansProps) => { ); if (resp?.data?.checkout_session?.checkout_url) { onSuccess(resp?.data?.checkout_session); - window.location.href = resp?.data?.checkout_session?.checkout_url; } } } catch (err: any) { From c130cc85d0bf8b7ef0257282cf735377cdfae4b0 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 2 Feb 2024 14:32:03 +0530 Subject: [PATCH 6/8] chore: add activeplan to frontier context --- .../core/react/contexts/FrontierContext.tsx | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/sdks/js/packages/core/react/contexts/FrontierContext.tsx b/sdks/js/packages/core/react/contexts/FrontierContext.tsx index 85d267a34b..a9dce75e67 100644 --- a/sdks/js/packages/core/react/contexts/FrontierContext.tsx +++ b/sdks/js/packages/core/react/contexts/FrontierContext.tsx @@ -19,6 +19,7 @@ import { V1Beta1BillingAccount, V1Beta1Group, V1Beta1Organization, + V1Beta1Plan, V1Beta1Subscription, V1Beta1User } from '../../client/data-contracts'; @@ -67,6 +68,12 @@ interface FrontierContextProviderProps { isActiveSubscriptionLoading: boolean; setIsActiveSubscriptionLoading: Dispatch>; + + activePlan: V1Beta1Plan | undefined; + setActivePlan: Dispatch>; + + isActivePlanLoading: boolean; + setIsActivePlanLoading: Dispatch>; } const defaultConfig: FrontierClientOptions = { @@ -117,7 +124,13 @@ const initialValues: FrontierContextProviderProps = { setActiveSubscription: () => undefined, isActiveSubscriptionLoading: false, - setIsActiveSubscriptionLoading: () => false + setIsActiveSubscriptionLoading: () => false, + + activePlan: undefined, + setActivePlan: () => undefined, + + isActivePlanLoading: false, + setIsActivePlanLoading: () => false }; export const FrontierContext = @@ -150,6 +163,9 @@ export const FrontierContextProvider = ({ const [activeSubscription, setActiveSubscription] = useState(); + const [activePlan, setActivePlan] = useState(); + const [isActivePlanLoading, setIsActivePlanLoading] = useState(false); + useEffect(() => { async function getFrontierInformation() { try { @@ -218,6 +234,28 @@ export const FrontierContextProvider = ({ } }, [getFrontierCurrentUserGroups, getFrontierCurrentUserOrganizations, user]); + const getPlan = useCallback( + async (planId: string) => { + setIsActivePlanLoading(true); + + try { + const resp = await frontierClient?.frontierServiceGetPlan(planId); + const plan = resp?.data?.plan; + if (plan) { + setActivePlan(plan); + } + } catch (err) { + console.error( + 'frontier:sdk:: There is problem with fetching active plan' + ); + console.error(err); + } finally { + setIsActivePlanLoading(false); + } + }, + [frontierClient] + ); + const getSubscription = useCallback( async (orgId: string, billingId: string) => { setIsActiveSubscriptionLoading(true); @@ -229,6 +267,9 @@ export const FrontierContextProvider = ({ if (resp?.data?.subscriptions?.length) { const activeSub = getActiveSubscription(resp?.data?.subscriptions); setActiveSubscription(activeSub); + if (activeSub?.plan_id) { + getPlan(activeSub?.plan_id); + } } else { setActiveSubscription(undefined); } @@ -241,7 +282,7 @@ export const FrontierContextProvider = ({ setIsActiveSubscriptionLoading(false); } }, - [frontierClient] + [frontierClient, getPlan] ); const getBillingAccount = useCallback( @@ -307,7 +348,11 @@ export const FrontierContextProvider = ({ isActiveSubscriptionLoading, setIsActiveSubscriptionLoading, activeSubscription, - setActiveSubscription + setActiveSubscription, + activePlan, + setActivePlan, + isActivePlanLoading, + setIsActivePlanLoading }} > {children} From d494cb5f0fc6a9fda8a8668ccbbef5481ab70aa4 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 2 Feb 2024 14:32:47 +0530 Subject: [PATCH 7/8] feat: add confirm modal for plan downgrade --- .../plans/confirm-change/index.tsx | 151 ++++++++++++++++++ .../components/organization/plans/index.tsx | 78 +++------ .../react/components/organization/profile.tsx | 9 +- sdks/js/packages/core/react/utils/index.ts | 22 ++- 4 files changed, 200 insertions(+), 60 deletions(-) create mode 100644 sdks/js/packages/core/react/components/organization/plans/confirm-change/index.tsx diff --git a/sdks/js/packages/core/react/components/organization/plans/confirm-change/index.tsx b/sdks/js/packages/core/react/components/organization/plans/confirm-change/index.tsx new file mode 100644 index 0000000000..f1e1716a2a --- /dev/null +++ b/sdks/js/packages/core/react/components/organization/plans/confirm-change/index.tsx @@ -0,0 +1,151 @@ +import { Dialog, Flex, Text, Image, Separator, Button } from '@raystack/apsara'; +import styles from '../../organization.module.css'; +import { useNavigate, useParams } from '@tanstack/react-router'; +import cross from '~/react/assets/cross.svg'; +import { useFrontier } from '~/react/contexts/FrontierContext'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import dayjs, { ManipulateType } from 'dayjs'; +import { DEFAULT_DATE_FORMAT } from '~/react/utils/constants'; +import { V1Beta1Plan } from '~/src'; +import Skeleton from 'react-loading-skeleton'; +import { getPlanChangeAction } from '~/react/utils'; +import planStyles from '../plans.module.css'; +import { usePlans } from '../hooks/usePlans'; + +export default function ConfirmPlanChange() { + const navigate = useNavigate({ from: '/plans/confirm-change/$planId' }); + const { planId } = useParams({ from: '/plans/confirm-change/$planId' }); + const { activePlan, isActivePlanLoading, config, client } = useFrontier(); + const [newPlan, setNewPlan] = useState(); + const [isNewPlanLoading, setIsNewPlanLoading] = useState(false); + + const { checkoutPlan, isLoading: isCheckoutLoading } = usePlans({ + onSuccess: data => { + window.location.href = data?.checkout_url as string; + } + }); + + function cancel() { + navigate({ to: '/plans' }); + } + + const expiryDate = useMemo(() => { + if (activePlan?.created_at && activePlan?.interval) { + return dayjs(activePlan?.created_at) + .add(1, activePlan?.interval as ManipulateType) + .format(config.dateFormat || DEFAULT_DATE_FORMAT); + } + return ''; + }, [activePlan?.created_at, activePlan?.interval, config.dateFormat]); + + function onConfirm() { + checkoutPlan(planId); + } + + const getPlan = useCallback( + async (planId: string) => { + setIsNewPlanLoading(true); + + try { + const resp = await client?.frontierServiceGetPlan(planId); + const plan = resp?.data?.plan; + if (plan) { + setNewPlan(plan); + } + } catch (err) { + console.error( + 'frontier:sdk:: There is problem with fetching active plan' + ); + console.error(err); + } finally { + setIsNewPlanLoading(false); + } + }, + [client] + ); + + useEffect(() => { + if (planId) { + getPlan(planId); + } + }, [getPlan, planId]); + + const isLoading = isActivePlanLoading || isNewPlanLoading; + + const newPlanMetadata = newPlan?.metadata as Record; + const activePlanMetadata = activePlan?.metadata as Record; + + const planAction = getPlanChangeAction( + Number(newPlanMetadata?.weightage) || 0, + Number(activePlanMetadata?.weightage) + ); + + return ( + + {/* @ts-ignore */} + + + {isLoading ? ( + + ) : ( + + Verify {planAction?.btnLabel} + + )} + + cross + + + + {isLoading ? ( + + ) : ( + + + Current plan: + + + {activePlan?.title} + + + )} + {isLoading ? ( + + ) : ( + + + New plan: + + + {newPlan?.title} (effective from {expiryDate}) + + + )} + + + + + + + + + ); +} diff --git a/sdks/js/packages/core/react/components/organization/plans/index.tsx b/sdks/js/packages/core/react/components/organization/plans/index.tsx index 0113cf1e5c..7c1e16920f 100644 --- a/sdks/js/packages/core/react/components/organization/plans/index.tsx +++ b/sdks/js/packages/core/react/components/organization/plans/index.tsx @@ -21,9 +21,10 @@ import { PlanIntervalPricing } from '~/src/types'; import checkCircle from '~/react/assets/check-circle.svg'; -import qs from 'query-string'; -import { getPlanChangeAction } from '~/react/utils'; +import { PlanChangeAction, getPlanChangeAction } from '~/react/utils'; import Amount from '../../helpers/Amount'; +import { Outlet, useNavigate } from '@tanstack/react-router'; +import { usePlans } from './hooks/usePlans'; const PlansLoader = () => { return ( @@ -87,9 +88,15 @@ const PlanPricingColumn = ({ featureMap: Record; currentPlan?: IntervalPricingWithPlan; }) => { - const { client, activeOrganization, billingAccount, config } = useFrontier(); + const { config } = useFrontier(); - const [isLoading, setIsLoading] = useState(false); + const navigate = useNavigate({ from: '/plans' }); + + const { checkoutPlan, isLoading } = usePlans({ + onSuccess: data => { + window.location.href = data?.checkout_url as string; + } + }); const planIntervals = Object.values(plan.intervals) @@ -111,7 +118,7 @@ const PlanPricingColumn = ({ const selectedIntervalPricing = plan.intervals[selectedInterval]; - const action = useMemo(() => { + const action: PlanChangeAction = useMemo(() => { if (selectedIntervalPricing.planId === currentPlan?.planId) { return { disabled: true, @@ -121,8 +128,8 @@ const PlanPricingColumn = ({ } const planAction = getPlanChangeAction( - selectedIntervalPricing, - currentPlan + selectedIntervalPricing.weightage, + currentPlan?.weightage ); return { disabled: false, @@ -130,55 +137,21 @@ const PlanPricingColumn = ({ }; }, [currentPlan, selectedIntervalPricing]); - const onPlanActionClick = useCallback(async () => { - setIsLoading(true); - try { - if (activeOrganization?.id && billingAccount?.id) { - const query = qs.stringify( - { - details: btoa( - qs.stringify({ - billing_id: billingAccount?.id, - organization_id: activeOrganization?.id, - type: 'plans' - }) - ), - checkout_id: '{{.CheckoutID}}' - }, - { encode: false } - ); - const cancel_url = `${config?.billing?.cancelUrl}?${query}`; - const success_url = `${config?.billing?.successUrl}?${query}`; - - const resp = await client?.frontierServiceCreateCheckout( - activeOrganization?.id, - billingAccount?.id, - { - cancel_url: cancel_url, - success_url: success_url, - subscription_body: { - plan: selectedIntervalPricing?.planId - } - } - ); - if (resp?.data?.checkout_session?.checkout_url) { - window.location.href = resp?.data?.checkout_session?.checkout_url; + const onPlanActionClick = useCallback(() => { + if (action?.showModal) { + navigate({ + to: '/plans/confirm-change/$planId', + params: { + planId: selectedIntervalPricing?.planId } - } - } catch (err: any) { - console.error(err); - toast.error('Something went wrong', { - description: err?.message }); - } finally { - setIsLoading(false); + } else { + checkoutPlan(selectedIntervalPricing?.planId); } }, [ - activeOrganization?.id, - billingAccount?.id, - config?.billing?.cancelUrl, - config?.billing?.successUrl, - client, + action?.showModal, + checkoutPlan, + navigate, selectedIntervalPricing?.planId ]); @@ -381,6 +354,7 @@ export default function Plans() { /> )} + ); } diff --git a/sdks/js/packages/core/react/components/organization/profile.tsx b/sdks/js/packages/core/react/components/organization/profile.tsx index fb8c20feec..a58a87046b 100644 --- a/sdks/js/packages/core/react/components/organization/profile.tsx +++ b/sdks/js/packages/core/react/components/organization/profile.tsx @@ -38,6 +38,7 @@ import { DeleteDomain } from './domain/delete'; import Billing from './billing'; import { EditBillingAddress } from './billing/address/edit'; import Plans from './plans'; +import ConfirmPlanChange from './plans/confirm-change'; interface OrganizationProfileProps { organizationId: string; @@ -239,6 +240,12 @@ const plansRoute = new Route({ component: Plans }); +const planDowngradeRoute = new Route({ + getParentRoute: () => plansRoute, + path: '/confirm-change/$planId', + component: ConfirmPlanChange +}); + const routeTree = rootRoute.addChildren([ indexRoute.addChildren([deleteOrgRoute]), securityRoute, @@ -255,7 +262,7 @@ const routeTree = rootRoute.addChildren([ profileRoute, preferencesRoute, billingRoute.addChildren([editBillingAddressRoute]), - plansRoute + plansRoute.addChildren([planDowngradeRoute]) ]); const router = new Router({ diff --git a/sdks/js/packages/core/react/utils/index.ts b/sdks/js/packages/core/react/utils/index.ts index 981ac4b9a7..0e80419412 100644 --- a/sdks/js/packages/core/react/utils/index.ts +++ b/sdks/js/packages/core/react/utils/index.ts @@ -2,7 +2,6 @@ import dayjs from 'dayjs'; import { V1Beta1Subscription, BillingAccountAddress } from '~/src'; import { IntervalPricingWithPlan } from '~/src/types'; import { SUBSCRIPTION_STATES } from './constants'; -import * as _ from 'lodash'; export const AuthTooltipMessage = 'You don’t have access to perform this action'; @@ -29,12 +28,20 @@ export const getActiveSubscription = (subscriptions: V1Beta1Subscription[]) => { return activeSubscriptions[0]; }; +export interface PlanChangeAction { + btnLabel: string; + btnLoadingLabel: string; + showModal?: boolean; + disabled?: boolean; +} + export const getPlanChangeAction = ( - nextPlan: IntervalPricingWithPlan, - currentPlan?: IntervalPricingWithPlan -) => { - const diff = nextPlan.weightage - (currentPlan?.weightage || 0); - if (diff > 0 || _.isEmpty(currentPlan)) { + nextPlanWeightage: number, + currentPlanWeightage?: number +): PlanChangeAction => { + const diff = nextPlanWeightage - (currentPlanWeightage || 0); + + if (diff > 0 || !currentPlanWeightage) { return { btnLabel: 'Upgrade', btnLoadingLabel: 'Upgrading' @@ -42,7 +49,8 @@ export const getPlanChangeAction = ( } else if (diff < 0) { return { btnLabel: 'Downgrade', - btnLoadingLabel: 'Downgrading' + btnLoadingLabel: 'Downgrading', + showModal: true }; } else { return { From 44f45081a0ed80e1b2494d7bb179a4bc8ea6ec93 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 2 Feb 2024 15:00:53 +0530 Subject: [PATCH 8/8] refactor: move onSuccess to function options --- .../organization/plans/confirm-change/index.tsx | 13 +++++++------ .../organization/plans/hooks/usePlans.tsx | 10 +++++----- .../react/components/organization/plans/index.tsx | 13 +++++++------ 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/sdks/js/packages/core/react/components/organization/plans/confirm-change/index.tsx b/sdks/js/packages/core/react/components/organization/plans/confirm-change/index.tsx index f1e1716a2a..646ca32680 100644 --- a/sdks/js/packages/core/react/components/organization/plans/confirm-change/index.tsx +++ b/sdks/js/packages/core/react/components/organization/plans/confirm-change/index.tsx @@ -19,11 +19,7 @@ export default function ConfirmPlanChange() { const [newPlan, setNewPlan] = useState(); const [isNewPlanLoading, setIsNewPlanLoading] = useState(false); - const { checkoutPlan, isLoading: isCheckoutLoading } = usePlans({ - onSuccess: data => { - window.location.href = data?.checkout_url as string; - } - }); + const { checkoutPlan, isLoading: isCheckoutLoading } = usePlans(); function cancel() { navigate({ to: '/plans' }); @@ -39,7 +35,12 @@ export default function ConfirmPlanChange() { }, [activePlan?.created_at, activePlan?.interval, config.dateFormat]); function onConfirm() { - checkoutPlan(planId); + checkoutPlan({ + planId, + onSuccess: data => { + window.location.href = data?.checkout_url as string; + } + }); } const getPlan = useCallback( diff --git a/sdks/js/packages/core/react/components/organization/plans/hooks/usePlans.tsx b/sdks/js/packages/core/react/components/organization/plans/hooks/usePlans.tsx index 7a54d2e733..8d8f5a7160 100644 --- a/sdks/js/packages/core/react/components/organization/plans/hooks/usePlans.tsx +++ b/sdks/js/packages/core/react/components/organization/plans/hooks/usePlans.tsx @@ -4,16 +4,17 @@ import qs from 'query-string'; import { toast } from 'sonner'; import { V1Beta1CheckoutSession } from '~/src'; -interface usePlansProps { +interface checkoutPlanOptions { + planId: string; onSuccess: (data: V1Beta1CheckoutSession) => void; } -export const usePlans = ({ onSuccess = () => {} }: usePlansProps) => { +export const usePlans = () => { const [isLoading, setIsLoading] = useState(false); const { client, activeOrganization, billingAccount, config } = useFrontier(); const checkoutPlan = useCallback( - async (planId: string) => { + async ({ planId, onSuccess }: checkoutPlanOptions) => { setIsLoading(true); try { if (activeOrganization?.id && billingAccount?.id) { @@ -62,8 +63,7 @@ export const usePlans = ({ onSuccess = () => {} }: usePlansProps) => { billingAccount?.id, config?.billing?.cancelUrl, config?.billing?.successUrl, - client, - onSuccess + client ] ); diff --git a/sdks/js/packages/core/react/components/organization/plans/index.tsx b/sdks/js/packages/core/react/components/organization/plans/index.tsx index 7c1e16920f..38589a6e6c 100644 --- a/sdks/js/packages/core/react/components/organization/plans/index.tsx +++ b/sdks/js/packages/core/react/components/organization/plans/index.tsx @@ -92,11 +92,7 @@ const PlanPricingColumn = ({ const navigate = useNavigate({ from: '/plans' }); - const { checkoutPlan, isLoading } = usePlans({ - onSuccess: data => { - window.location.href = data?.checkout_url as string; - } - }); + const { checkoutPlan, isLoading } = usePlans(); const planIntervals = Object.values(plan.intervals) @@ -146,7 +142,12 @@ const PlanPricingColumn = ({ } }); } else { - checkoutPlan(selectedIntervalPricing?.planId); + checkoutPlan({ + planId: selectedIntervalPricing?.planId, + onSuccess: data => { + window.location.href = data?.checkout_url as string; + } + }); } }, [ action?.showModal,