Skip to content

Commit

Permalink
feat(sdk): add support for zero value base plan (#674)
Browse files Browse the repository at this point in the history
* chore: link sdk with demo app

* chore: export sdk client options

* feat: add base plan in sdk config

* chore: merge plans with base plan

* fix: handle undefined currency and value

* fix: handle undefined value

* feat: add nil uuid to base plan

* fix: handle undefined plan weigtage

* chore: add baseplan to frontier context

* refactor: move enrichBasePlan to react utils

* chore: use context base plan instead of config base plan

* fix: handle downgrade to base plan

* fix: show base plan as current plan if user is not subscribed

* feat: show base plan in billing screen

* feat: show banner with base plan

* refactor: remove duplicate function

* fix: add check for provider id in offline billing account for upcoming invoice

* chore: update proto client

* fix: show downgrade banner when subscription is cancel

* feat: add features to base plan

* fix: make features optional type

* refactor: make logic more readable
  • Loading branch information
rsbh authored Jul 25, 2024
1 parent 2e297c7 commit 2a6b630
Show file tree
Hide file tree
Showing 18 changed files with 255 additions and 152 deletions.
1 change: 1 addition & 0 deletions sdks/js/packages/core/client/V1Beta1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1729,6 +1729,7 @@ export class V1Beta1<SecurityDataType = unknown> extends HttpClient<SecurityData
query?: {
source?: string;
action?: string;
ignore_system?: boolean;
/**
* start_time and end_time are inclusive
* @format date-time
Expand Down
3 changes: 2 additions & 1 deletion sdks/js/packages/core/client/data-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface SubscriptionPhase {
/** @format date-time */
effective_at?: string;
plan_id?: string;
reason?: string;
}

export interface WebhookSecret {
Expand All @@ -79,7 +80,7 @@ export interface ProtobufAny {
* `NullValue` is a singleton enumeration to represent the null value for the
* `Value` type union.
*
* The JSON representation for `NullValue` is JSON `null`.
* The JSON representation for `NullValue` is JSON `null`.
*
* - NULL_VALUE: Null value.
* @default "NULL_VALUE"
Expand Down
2 changes: 2 additions & 0 deletions sdks/js/packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"@types/node": "^20.6.3",
"@types/react": "^18.2.22",
"@types/react-dom": "^18.2.7",
"@types/uuid": "^10.0.0",
"esbuild-css-modules-plugin": "^2.7.1",
"esbuild-plugin-external-global": "^1.0.1",
"eslint": "^7.32.0",
Expand Down Expand Up @@ -91,6 +92,7 @@
"react-loading-skeleton": "^3.3.1",
"slugify": "^1.6.6",
"sonner": "^0.6.2",
"uuid": "^10.0.0",
"yup": "^1.2.0"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Button, Flex, Text } from '@raystack/apsara';
import Skeleton from 'react-loading-skeleton';
import { DEFAULT_DATE_FORMAT } from '~/react/utils/constants';
import {
DEFAULT_DATE_FORMAT,
SUBSCRIPTION_STATES
} from '~/react/utils/constants';
import { V1Beta1Plan, V1Beta1Subscription } from '~/src';
import styles from './styles.module.css';
import { InfoCircledIcon } from '@radix-ui/react-icons';
Expand Down Expand Up @@ -32,7 +35,8 @@ export function UpcomingPlanChangeBanner({
activePlan,
activeOrganization,
billingAccount,
fetchActiveSubsciption
fetchActiveSubsciption,
basePlan
} = useFrontier();
const [upcomingPlan, setUpcomingPlan] = useState<V1Beta1Plan>();
const [isPlanLoading, setIsPlanLoading] = useState(false);
Expand Down Expand Up @@ -81,7 +85,7 @@ export function UpcomingPlanChangeBanner({
const activePlanMetadata = activePlan?.metadata as Record<string, number>;

const planAction = getPlanChangeAction(
Number(newPlanMetadata?.weightage) || 0,
Number(newPlanMetadata?.weightage),
Number(activePlanMetadata?.weightage)
);

Expand Down Expand Up @@ -121,7 +125,7 @@ export function UpcomingPlanChangeBanner({
]);

const currentPlanName = getPlanNameWithInterval(activePlan);
const upcomingPlanName = getPlanNameWithInterval(upcomingPlan);
const upcomingPlanName = getPlanNameWithInterval(upcomingPlan || basePlan);

const areSimilarPlans = checkSimilarPlans(
activePlan || {},
Expand All @@ -132,9 +136,14 @@ export function UpcomingPlanChangeBanner({
? getPlanIntervalName(activePlan)
: activePlan?.title;

const showBanner =
nextPhase?.plan_id ||
(subscription?.state === SUBSCRIPTION_STATES.ACTIVE &&
nextPhase?.reason === 'cancel');

return showLoader ? (
<Skeleton />
) : nextPhase?.plan_id ? (
) : showBanner ? (
<Flex className={styles.changeBannerBox} justify={'between'}>
<Flex gap="small" className={styles.flex1} align={'center'}>
<InfoCircledIcon className={styles.currentPlanInfoText} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const currencyDecimalMap: Record<string, number> = {
const DEFAULT_DECIMAL = 0;

export default function Amount({
currency,
value,
currency = 'usd',
value = 0,
className,
currencyClassName,
decimalClassName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useNavigate } from '@tanstack/react-router';
import { ReactNode, useEffect, useMemo, useState } from 'react';
import { ReactNode, useEffect, useState } from 'react';
import { useFrontier } from '~/react/contexts/FrontierContext';
import { V1Beta1Invoice, V1Beta1Plan } from '~/src';
import { toast } from 'sonner';
Expand All @@ -11,7 +11,6 @@ import Amount from '../../helpers/Amount';
import dayjs from 'dayjs';
import { InfoCircledIcon } from '@radix-ui/react-icons';
import {
getPlanChangeAction,
getPlanIntervalName,
getPlanNameWithInterval,
makePlanSlug
Expand Down Expand Up @@ -88,7 +87,8 @@ export const UpcomingBillingCycle = ({
billingAccount,
config,
activeSubscription,
isActiveOrganizationLoading
isActiveOrganizationLoading,
basePlan
} = useFrontier();
const [isInvoiceLoading, setIsInvoiceLoading] = useState(false);
const [memberCount, setMemberCount] = useState(0);
Expand Down Expand Up @@ -127,33 +127,6 @@ export const UpcomingBillingCycle = ({
}
}, [client, activeSubscription?.plan_id]);

useEffect(() => {
async function getUpcomingInvoice(orgId: string, billingId: string) {
setIsInvoiceLoading(true);
try {
const resp = await client?.frontierServiceGetUpcomingInvoice(
orgId,
billingId
);
const invoice = resp?.data?.invoice;
if (invoice && invoice.state) {
setUpcomingInvoice(invoice);
}
} catch (err: any) {
toast.error('Something went wrong', {
description: err.message
});
console.error(err);
} finally {
setIsInvoiceLoading(false);
}
}

if (billingAccount?.id && billingAccount?.org_id) {
getUpcomingInvoice(billingAccount?.org_id, billingAccount?.id);
}
}, [client, billingAccount?.org_id, billingAccount?.id]);

useEffect(() => {
async function getMemberCount(orgId: string) {
setIsMemberCountLoading(true);
Expand Down Expand Up @@ -181,7 +154,7 @@ export const UpcomingBillingCycle = ({
billingId
);
const invoice = resp?.data?.invoice;
if (invoice) {
if (invoice && invoice.state) {
setUpcomingInvoice(invoice);
}
} catch (err: any) {
Expand All @@ -194,11 +167,20 @@ export const UpcomingBillingCycle = ({
}
}

if (billingAccount?.id && billingAccount?.org_id) {
if (
billingAccount?.id &&
billingAccount?.org_id &&
billingAccount?.provider_id
) {
getUpcomingInvoice(billingAccount?.org_id, billingAccount?.id);
getMemberCount(billingAccount?.org_id);
}
}, [client, billingAccount?.org_id, billingAccount?.id]);
}, [
client,
billingAccount?.org_id,
billingAccount?.id,
billingAccount?.provider_id
]);

const planName = getPlanNameWithInterval(plan);

Expand All @@ -210,6 +192,14 @@ export const UpcomingBillingCycle = ({
link: '/plans'
}
}
: basePlan
? {
message: `You are subscribed to ${basePlan?.title}.`,
action: {
label: 'Upgrade',
link: '/plans'
}
}
: {
message: 'You are not subscribed to any plan',
action: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,27 @@ export default function ConfirmPlanChange() {
config,
client,
fetchActiveSubsciption,
activeSubscription
activeSubscription,
basePlan
} = useFrontier();
const [newPlan, setNewPlan] = useState<V1Beta1Plan>();
const [isNewPlanLoading, setIsNewPlanLoading] = useState(false);

const {
changePlan,
isLoading: isChangePlanLoading,
verifyPlanChange
verifyPlanChange,
cancelSubscription,
checkBasePlan
} = usePlans();

const isNewPlanBasePlan = checkBasePlan(planId);

const newPlanMetadata = newPlan?.metadata as Record<string, number>;
const activePlanMetadata = activePlan?.metadata as Record<string, number>;

const planAction = getPlanChangeAction(
Number(newPlanMetadata?.weightage) || 0,
Number(newPlanMetadata?.weightage),
Number(activePlanMetadata?.weightage)
);

Expand Down Expand Up @@ -91,20 +96,34 @@ export default function ConfirmPlanChange() {
verifyPlanChange
]);

const onConfirm = useCallback(() => {
changePlan({
planId,
onSuccess: verifyChange,
immediate: planAction.immediate
});
}, [changePlan, planId, planAction.immediate, verifyChange]);
const onConfirm = useCallback(async () => {
if (isNewPlanBasePlan) {
cancelSubscription({
onSuccess: verifyChange
});
} else {
changePlan({
planId,
onSuccess: verifyChange,
immediate: planAction.immediate
});
}
}, [
isNewPlanBasePlan,
cancelSubscription,
verifyChange,
changePlan,
planId,
planAction.immediate
]);

const getPlan = useCallback(
async (planId: string) => {
setIsNewPlanLoading(true);

try {
const resp = await client?.frontierServiceGetPlan(planId);
const resp = isNewPlanBasePlan
? { data: { plan: basePlan } }
: await client?.frontierServiceGetPlan(planId);
const plan = resp?.data?.plan;
if (plan) {
setNewPlan(plan);
Expand All @@ -118,7 +137,7 @@ export default function ConfirmPlanChange() {
setIsNewPlanLoading(false);
}
},
[client]
[isNewPlanBasePlan, basePlan, client]
);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { toast } from 'sonner';
import { SubscriptionPhase, V1Beta1CheckoutSession, V1Beta1Plan } from '~/src';
import { SUBSCRIPTION_STATES } from '~/react/utils/constants';
import { PlanMetadata } from '~/src/types';
import { NIL as NIL_UUID } from 'uuid';

interface checkoutPlanOptions {
isTrial: boolean;
Expand All @@ -18,6 +19,10 @@ interface changePlanOptions {
onSuccess: () => void;
}

interface cancelSubscriptionOptions {
onSuccess: () => void;
}

interface verifyPlanChangeOptions {
planId: string;
onSuccess?: (planPhase: SubscriptionPhase) => void;
Expand Down Expand Up @@ -110,6 +115,10 @@ export const usePlans = () => {
]
);

const checkBasePlan = (planId: string) => {
return planId === NIL_UUID;
};

const changePlan = useCallback(
async ({ planId, onSuccess, immediate = false }: changePlanOptions) => {
setIsLoading(true);
Expand Down Expand Up @@ -151,7 +160,9 @@ export const usePlans = () => {
const activeSub = await fetchActiveSubsciption();
if (activeSub) {
const planPhase = activeSub.phases?.find(
phase => phase?.plan_id === planId
phase =>
phase?.plan_id === planId ||
(planId === NIL_UUID && phase?.plan_id === '')
);
if (planPhase) {
onSuccess(planPhase);
Expand Down Expand Up @@ -218,6 +229,37 @@ export const usePlans = () => {
[getIneligiblePlansIdsSetForTrial, getSubscribedPlans]
);

const cancelSubscription = useCallback(
async ({ onSuccess }: cancelSubscriptionOptions) => {
setIsLoading(true);
try {
if (
activeOrganization?.id &&
billingAccount?.id &&
activeSubscription?.id
) {
const resp = await client?.frontierServiceCancelSubscription(
activeOrganization?.id,
billingAccount?.id,
activeSubscription?.id,
{ immediate: false }
);
if (resp?.data) {
onSuccess();
}
}
} catch (err: any) {
console.error(err);
toast.error('Something went wrong', {
description: err?.message
});
} finally {
setIsLoading(false);
}
},
[activeOrganization?.id, billingAccount?.id, activeSubscription?.id, client]
);

return {
checkoutPlan,
isLoading,
Expand All @@ -227,6 +269,8 @@ export const usePlans = () => {
hasAlreadyTrialed,
isCurrentlyTrialing,
checkAlreadyTrialed,
subscriptions
subscriptions,
cancelSubscription,
checkBasePlan
};
};
Loading

0 comments on commit 2a6b630

Please sign in to comment.