Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbitRelease Notes
WalkthroughThis PR introduces comprehensive billing and plans management features to the admin and client SDKs. It adds a BillingView component with billing details, payment methods, and invoice management; a PlansView with plan selection and interval pricing; supporting dialogs and forms; and integrates these views into the client-demo app's organization settings routing. It also corrects a typo in the billing address helper function name. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Coverage Report for CI Build 24325869432Coverage remained the same at 41.606%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (15)
web/sdk/react/views-new/billing/components/billing-details-card.tsx (1)
36-58: Tooltip logic is correct but could be clearer.The
disabled={!isButtonDisabled}onTooltip.Triggermeans the tooltip only activates when the button is disabled. This is correct behavior, but the double-negative makes it harder to read. Consider extracting to a named variable:const showTooltip = isButtonDisabled; // ... <Tooltip.Trigger disabled={!showTooltip} ...>web/sdk/react/views-new/billing/components/upcoming-plan-change-banner.tsx (2)
12-13: Consolidate duplicate imports from the same module.The imports from
@connectrpc/connect-queryare split across two lines. Consolidate them for cleaner code.♻️ Suggested fix
-import { useMutation } from '@connectrpc/connect-query'; -import { createConnectQueryKey, useTransport } from '@connectrpc/connect-query'; +import { useMutation, createConnectQueryKey, useTransport } from '@connectrpc/connect-query';
96-102: Unsafe type assertion on metadata.Casting
metadataasRecord<string, number>assumes all values are numbers, but metadata is typically a flexible key-value store that may contain strings. Ifweightageis stored as a string,Number()will handle it, but consider using a safer approach or validating the type.web/sdk/react/views-new/plans/components/confirm-plan-change-dialog.tsx (2)
177-187: Unnecessary loading state for synchronous operation.The
setIsNewPlanLoading(true/false)wraps a synchronousArray.find()operation. This will cause an unnecessary render cycle and the loading state will never be visible to users.♻️ Suggested fix
useEffect(() => { if (planId) { - setIsNewPlanLoading(true); - try { - const plan = isNewPlanBasePlan ? basePlan : currentPlan; - if (plan) setNewPlan(plan); - } finally { - setIsNewPlanLoading(false); - } + const plan = isNewPlanBasePlan ? basePlan : currentPlan; + if (plan) setNewPlan(plan); } }, [planId, isNewPlanBasePlan, basePlan, currentPlan]);Then remove
isNewPlanLoadingfrom theisLoadingcomputation on line 189.
102-102: Fragile string comparison for determining upgrade status.Comparing
planAction.btnLabel === 'Upgrade'couples business logic to UI text. If the label is localized or changed, this breaks.Consider using a semantic property like
planAction.isUpgradeor comparing weightage directly.web/sdk/react/views-new/billing/components/confirm-cycle-switch-dialog.tsx (2)
176-209:onConfirmshould be memoized withuseCallback.This async function is defined inside the component body without memoization, causing it to be recreated on every render. While not critical since it's only passed to an
onClickhandler, it's inconsistent with patterns in other dialogs.♻️ Suggested fix
- async function onConfirm() { + const onConfirm = useCallback(async () => { const nextPlanId = nextPlan?.id; if (!nextPlanId) return; // ... rest of function - } + }, [nextPlan?.id, isPaymentMethodRequired, checkoutPlan, changePlan, verifyPlanChange, handle, nextPlanIntervalName, dateFormat, isUpgrade]);
246-248: Limited currency symbol formatting.Only USD gets the
$symbol. Other currencies will display as{amount}without any symbol. Consider usingIntl.NumberFormator theAmountcomponent used elsewhere for consistent currency formatting.web/sdk/react/views-new/billing/components/invoices.tsx (1)
29-95: Consider guarding against NaN when coercing amount.On line 64, if
getValue()returnsundefinedornull,Number()will produceNaN. TheAmountcomponent may not handle this gracefully.🛡️ Suggested defensive check
cell: ({ row, getValue }) => { - const value = Number(getValue()); + const rawValue = getValue(); + const value = rawValue != null ? Number(rawValue) : 0; return ( <Text size="regular" variant="secondary"> <Amount currency={row?.original?.currency} value={value} /> </Text> ); }web/sdk/react/views-new/billing/billing-view.tsx (1)
27-29: Module-scope dialog handles are shared across component instances.These handles are created at module scope, meaning all instances of
BillingViewshare the same dialog state. This is typically fine for singleton views but could cause unexpected behavior if multipleBillingViewinstances are rendered simultaneously.web/sdk/react/views-new/plans/plans-view.tsx (3)
52-60: Consider adding error handling for features query.Unlike the billing view which handles invoice fetch errors with a toast, this component silently ignores errors from
listFeatures. If the features query fails, the UI might show an incomplete feature table without user feedback.🛡️ Suggested error handling
- const { data: featuresData } = useQuery( + const { data: featuresData, error: featuresError } = useQuery( FrontierServiceQueries.listFeatures, create(ListFeaturesRequestSchema, {}) ); + + useEffect(() => { + if (featuresError) { + toastManager.add({ + title: 'Failed to load plan features', + description: featuresError?.message, + type: 'error' + }); + } + }, [featuresError]);
104-123: Potential stale closure:selectedIntervalsread inside effect but not in deps.The effect reads
selectedIntervals[plan.slug]on line 109 to skip already-set intervals, butselectedIntervalsis not in the dependency array. On subsequent runs, the effect sees a stale snapshot.This may be intentional (only initialize defaults once per plan), but it can cause subtle bugs if the logic changes. Consider using a functional update or ref if this is intentional.
♻️ Using functional update to avoid stale closure
useEffect(() => { if (groupedPlans.length === 0) return; - const defaults: Record<string, IntervalKeys> = {}; - groupedPlans.forEach(plan => { - if (selectedIntervals[plan.slug]) return; - const planIntervals = Object.values(plan.intervals) - .sort((a, b) => a.weightage - b.weightage) - .map(i => i.interval); - const activePlanInterval = Object.values(plan.intervals).find( - p => p.planId === activeSubscription?.planId - ); - defaults[plan.slug] = - activePlanInterval?.interval || planIntervals[0] || 'year'; - }); - - if (Object.keys(defaults).length > 0) { - setSelectedIntervals(prev => ({ ...prev, ...defaults })); - } + setSelectedIntervals(prev => { + const defaults: Record<string, IntervalKeys> = {}; + groupedPlans.forEach(plan => { + if (prev[plan.slug]) return; + const planIntervals = Object.values(plan.intervals) + .sort((a, b) => a.weightage - b.weightage) + .map(i => i.interval); + const activePlanInterval = Object.values(plan.intervals).find( + p => p.planId === activeSubscription?.planId + ); + defaults[plan.slug] = + activePlanInterval?.interval || planIntervals[0] || 'year'; + }); + return Object.keys(defaults).length > 0 ? { ...prev, ...defaults } : prev; + }); }, [groupedPlans, activeSubscription?.planId]);
125-132: Consider memoizingcurrentPlanPricing.This nested loop runs on every render. While the dataset is likely small, memoizing it would be more consistent with the other derived values in this component.
♻️ Memoized version
- let currentPlanPricing: IntervalPricingWithPlan | undefined; - groupedPlans.forEach(group => { - Object.values(group.intervals).forEach(plan => { - if (plan.planId === activeSubscription?.planId) { - currentPlanPricing = plan; - } - }); - }); + const currentPlanPricing = useMemo(() => { + for (const group of groupedPlans) { + for (const plan of Object.values(group.intervals)) { + if (plan.planId === activeSubscription?.planId) { + return plan; + } + } + } + return undefined; + }, [groupedPlans, activeSubscription?.planId]);web/sdk/react/views-new/billing/components/upcoming-billing-cycle.tsx (3)
70-100: Inconsistent org ID usage between queries.The invoice query uses
activeOrganization?.id(line 77), while the member count query usesbillingAccount?.orgId(line 94). These should typically be the same, but if they ever differ, it could cause subtle bugs.Consider using a consistent source for the organization ID.
134-142: Error toast may fire multiple times for different errors.The
errorvariable combines bothmemberCountErrorandinvoiceErrorusing||. If both errors occur, only the first truthy one is displayed. Additionally, if one error clears and another appears, the effect will fire again.Consider handling each error independently or using a more robust error aggregation strategy.
♻️ Handle errors independently
- const error = memberCountError || invoiceError; - useEffect(() => { - if (error) { - toastManager.add({ - title: 'Failed to get upcoming billing cycle details', - type: 'error' - }); - } - }, [error]); + useEffect(() => { + if (invoiceError) { + toastManager.add({ + title: 'Failed to load upcoming invoice', + type: 'error' + }); + } + }, [invoiceError]); + + useEffect(() => { + if (memberCountError) { + toastManager.add({ + title: 'Failed to load member count', + type: 'error' + }); + } + }, [memberCountError]);
154-154: Consider specifying skeleton dimensions for better loading UX.A bare
<Skeleton />may render with default dimensions. Specifyingwidthandheightwould provide a more predictable placeholder that matches the final rendered content.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 100c21b4-2bf0-4923-b853-0c10014c94d8
📒 Files selected for processing (28)
web/apps/admin/src/utils/helper.tsweb/apps/client-demo/src/Router.tsxweb/apps/client-demo/src/pages/Settings.tsxweb/apps/client-demo/src/pages/settings/Billing.tsxweb/apps/client-demo/src/pages/settings/Plans.tsxweb/sdk/admin/utils/helper.tsweb/sdk/admin/views/organizations/details/side-panel/billing-details-section.tsxweb/sdk/react/index.tsweb/sdk/react/utils/index.tsweb/sdk/react/views-new/billing/billing-view.module.cssweb/sdk/react/views-new/billing/billing-view.tsxweb/sdk/react/views-new/billing/components/billing-details-card.tsxweb/sdk/react/views-new/billing/components/billing-details-dialog.tsxweb/sdk/react/views-new/billing/components/confirm-cycle-switch-dialog.tsxweb/sdk/react/views-new/billing/components/invoices.tsxweb/sdk/react/views-new/billing/components/payment-issue.tsxweb/sdk/react/views-new/billing/components/payment-method-card.tsxweb/sdk/react/views-new/billing/components/upcoming-billing-cycle.tsxweb/sdk/react/views-new/billing/components/upcoming-plan-change-banner.tsxweb/sdk/react/views-new/billing/index.tsweb/sdk/react/views-new/plans/components/confirm-plan-change-dialog.tsxweb/sdk/react/views-new/plans/components/feature-table.module.cssweb/sdk/react/views-new/plans/components/feature-table.tsxweb/sdk/react/views-new/plans/components/plan-card.module.cssweb/sdk/react/views-new/plans/components/plan-card.tsxweb/sdk/react/views-new/plans/index.tsweb/sdk/react/views-new/plans/plans-view.module.cssweb/sdk/react/views-new/plans/plans-view.tsx
| taxId: billingAccount.taxData?.[0]?.id || '' | ||
| }); | ||
| } | ||
| }, [billingAccount, reset]); | ||
|
|
||
| async function onSubmit(data: BillingDetailsFormData) { | ||
| if (!billingAccount?.id) return; | ||
| try { | ||
| const taxData = data.taxId | ||
| ? [ | ||
| create(BillingAccount_TaxSchema, { | ||
| id: data.taxId, | ||
| type: billingAccount.taxData?.[0]?.type || '' | ||
| }) | ||
| ] | ||
| : []; |
There was a problem hiding this comment.
Potential data loss with multiple tax entries.
The form only handles the first tax entry (taxData?.[0]). If billingAccount.taxData contains multiple entries, only the first one is preserved and re-submitted, silently discarding the rest.
If the API supports multiple tax IDs, consider handling them as an array field. If only one is ever expected, this is fine but worth documenting.
| onSuccess: data => { | ||
| window.location.href = data?.checkoutUrl as string; | ||
| } |
There was a problem hiding this comment.
Potential navigation to undefined URL.
Same issue as in confirm-plan-change-dialog.tsx - if data?.checkoutUrl is undefined, this will navigate to the string "undefined".
🛡️ Suggested fix
onSuccess: data => {
- window.location.href = data?.checkoutUrl as string;
+ if (data?.checkoutUrl) {
+ window.location.href = data.checkoutUrl;
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onSuccess: data => { | |
| window.location.href = data?.checkoutUrl as string; | |
| } | |
| onSuccess: data => { | |
| if (data?.checkoutUrl) { | |
| window.location.href = data.checkoutUrl; | |
| } | |
| } |
| const onRetryPayment = useCallback(() => { | ||
| window.location.href = openInvoices[0]?.hostedUrl || ''; | ||
| }, [openInvoices]); |
There was a problem hiding this comment.
Guard retry navigation when no hosted invoice URL is available.
At Line 33, falling back to '' can trigger an unintended reload instead of a payment retry path.
Proposed fix
+ const retryUrl = openInvoices[0]?.hostedUrl;
+
const onRetryPayment = useCallback(() => {
- window.location.href = openInvoices[0]?.hostedUrl || '';
- }, [openInvoices]);
+ if (!retryUrl) return;
+ window.location.href = retryUrl;
+ }, [retryUrl]);
...
<Button
variant="text"
color="neutral"
size="small"
onClick={onRetryPayment}
+ disabled={!retryUrl}
data-test-id="frontier-sdk-retry-payment-btn"
>Also applies to: 47-53
| async function updatePaymentMethod() { | ||
| const query = qs.stringify( | ||
| { | ||
| details: btoa( | ||
| qs.stringify({ | ||
| organization_id: activeOrganization?.id, | ||
| type: 'billing' | ||
| }) | ||
| ), | ||
| checkout_id: '{{.CheckoutID}}' | ||
| }, | ||
| { encode: false } | ||
| ); | ||
| const cancel_url = `${config?.billing?.cancelUrl}?${query}`; | ||
| const success_url = `${config?.billing?.successUrl}?${query}`; | ||
|
|
||
| const resp = await createCheckoutMutation( | ||
| create(CreateCheckoutRequestSchema, { | ||
| orgId: activeOrganization?.id || '', | ||
| cancelUrl: cancel_url, | ||
| successUrl: success_url, | ||
| setupBody: { | ||
| paymentMethod: true, | ||
| customerPortal: false | ||
| } | ||
| }) | ||
| ); |
There was a problem hiding this comment.
Validate checkout prerequisites before firing mutation.
At Line 76, an empty orgId and/or missing billing URLs can still reach createCheckout, causing avoidable mutation failures and noisy error toasts.
Proposed fix
async function updatePaymentMethod() {
+ if (
+ !activeOrganization?.id ||
+ !config?.billing?.cancelUrl ||
+ !config?.billing?.successUrl
+ ) {
+ toastManager.add({
+ title: 'Billing configuration missing',
+ description: 'Unable to start checkout. Please contact support.',
+ type: 'error'
+ });
+ return;
+ }
+
const query = qs.stringify(
{
details: btoa(
qs.stringify({
organization_id: activeOrganization?.id,
type: 'billing'
})
),
checkout_id: '{{.CheckoutID}}'
},
{ encode: false }
);Also applies to: 91-91
| {isAllowed ? ( | ||
| <Tooltip> | ||
| <Tooltip.Trigger render={<span />}> | ||
| <Button | ||
| variant="outline" | ||
| color="neutral" | ||
| size="small" | ||
| onClick={updatePaymentMethod} | ||
| disabled={isBtnDisabled} | ||
| data-test-id="frontier-sdk-update-payment-method-btn" | ||
| > | ||
| {isPaymentMethodAvailable ? 'Update' : 'Add method'} | ||
| </Button> | ||
| </Tooltip.Trigger> | ||
| <Tooltip.Content> | ||
| {AuthTooltipMessage} | ||
| </Tooltip.Content> | ||
| </Tooltip> | ||
| ) : null} |
There was a problem hiding this comment.
Authorization tooltip is shown in the allowed branch.
At Line 99, isAllowed currently renders a tooltip whose content says the user lacks access, which is contradictory and confusing.
Proposed fix
- {isAllowed ? (
- <Tooltip>
- <Tooltip.Trigger render={<span />}>
- <Button
- variant="outline"
- color="neutral"
- size="small"
- onClick={updatePaymentMethod}
- disabled={isBtnDisabled}
- data-test-id="frontier-sdk-update-payment-method-btn"
- >
- {isPaymentMethodAvailable ? 'Update' : 'Add method'}
- </Button>
- </Tooltip.Trigger>
- <Tooltip.Content>
- {AuthTooltipMessage}
- </Tooltip.Content>
- </Tooltip>
- ) : null}
+ {isAllowed ? (
+ <Button
+ variant="outline"
+ color="neutral"
+ size="small"
+ onClick={updatePaymentMethod}
+ disabled={isBtnDisabled}
+ data-test-id="frontier-sdk-update-payment-method-btn"
+ >
+ {isPaymentMethodAvailable ? 'Update' : 'Add method'}
+ </Button>
+ ) : (
+ <Tooltip>
+ <Tooltip.Trigger render={<span />}>
+ <Button variant="outline" color="neutral" size="small" disabled>
+ {isPaymentMethodAvailable ? 'Update' : 'Add method'}
+ </Button>
+ </Tooltip.Trigger>
+ <Tooltip.Content>{AuthTooltipMessage}</Tooltip.Content>
+ </Tooltip>
+ )}| <Tooltip> | ||
| <Tooltip.Trigger render={<span />}> | ||
| <InfoCircledIcon /> | ||
| </Tooltip.Trigger> | ||
| <Tooltip.Content> | ||
| {NEGATIVE_BALANCE_TOOLTIP_MESSAGE} | ||
| </Tooltip.Content> | ||
| </Tooltip> | ||
| ) : null} |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
@raystack/apsara-v1 Tooltip component API render prop usage
💡 Result:
The Tooltip component in @raystack/apsara-v1 uses a render prop on the Tooltip.Trigger subcomponent to specify the trigger element. ## Basic Usage import { Tooltip } from '@raystack/apsara'; <Tooltip.Trigger render={Hover me} /> <Tooltip.Content>Helpful information</Tooltip.Content> ## API for Tooltip.Trigger - render: The element that triggers the tooltip. Must be a renderable element (ReactElement). The render prop allows passing any React element as the trigger, such as buttons, icons, or custom components. This provides flexibility for custom trigger rendering while maintaining tooltip behavior on hover/focus. ## Full Structure - Tooltip (root): Manages state (open, onOpenChange, delay) - Tooltip.Trigger: Uses render prop for the trigger element; optional delay override - Tooltip.Content: Displays content (showArrow, side, align, sideOffset, className, style) - Tooltip.Provider: Shared config for multiple tooltips (delay) Examples include controlled tooltips, positioned tooltips, and provider usage, all consistently using Tooltip.Trigger render={...}.
Citations:
- 1: https://mintlify.com/raystack/apsara/components/tooltip
- 2: https://www.mintlify.com/raystack/apsara/components/tooltip
- 3: https://raystack-apsara.mintlify.app/components/tooltip
Fix Tooltip.Trigger usage: pass the icon via render prop, not as a child.
The Tooltip.Trigger component uses a render prop to specify the trigger element itself. The current pattern combines render={<span />} with a nested child <InfoCircledIcon />, which is incorrect. The render prop should contain the complete trigger element. Use either render={<InfoCircledIcon />} or render={<span><InfoCircledIcon /></span>} instead.
| Your {currentPlanName} will be{' '} | ||
| {planAction?.btnDoneLabel.toLowerCase()} to {upcomingPlanName} from{' '} | ||
| {expiryDate}. | ||
| </Text> |
There was a problem hiding this comment.
Potential runtime error if planAction is undefined.
planAction?.btnDoneLabel.toLowerCase() will throw if btnDoneLabel is undefined or if planAction is undefined and the optional chain doesn't protect the entire expression.
Consider adding a fallback:
🛡️ Suggested fix
<Text size="small">
Your {currentPlanName} will be{' '}
- {planAction?.btnDoneLabel.toLowerCase()} to {upcomingPlanName} from{' '}
+ {planAction?.btnDoneLabel?.toLowerCase() ?? 'changed'} to {upcomingPlanName} from{' '}
{expiryDate}.
</Text>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Your {currentPlanName} will be{' '} | |
| {planAction?.btnDoneLabel.toLowerCase()} to {upcomingPlanName} from{' '} | |
| {expiryDate}. | |
| </Text> | |
| Your {currentPlanName} will be{' '} | |
| {planAction?.btnDoneLabel?.toLowerCase() ?? 'changed'} to {upcomingPlanName} from{' '} | |
| {expiryDate}. | |
| </Text> |
| } else { | ||
| checkoutPlan({ | ||
| planId, | ||
| isTrial: false, | ||
| onSuccess: data => { | ||
| window.location.href = data?.checkoutUrl as string; | ||
| } | ||
| }); |
There was a problem hiding this comment.
Potential navigation to undefined URL.
If checkoutPlan succeeds but data?.checkoutUrl is undefined, window.location.href will be set to "undefined" (string), causing a broken navigation.
🛡️ Suggested fix
checkoutPlan({
planId,
isTrial: false,
onSuccess: data => {
- window.location.href = data?.checkoutUrl as string;
+ if (data?.checkoutUrl) {
+ window.location.href = data.checkoutUrl;
+ }
}
});| const value = productMetaDataFeatureValues?.[0] || '-'; | ||
| const isAvailable = value?.toLowerCase() === 'true'; | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Feature type definitions =="
rg -n -C3 'interface Feature|type Feature|metadata' web/sdk/src/types.ts || true
echo
echo "== Plans feature metadata read/write patterns =="
rg -n -C2 'productNames|features|metadata' web/sdk/react/views-new/plans web/sdk/src || trueRepository: raystack/frontier
Length of output: 7619
🏁 Script executed:
rg -n -B5 -A10 'const value = productMetaDataFeatureValues' web/sdk/react/views-new/plans/components/feature-table.tsxRepository: raystack/frontier
Length of output: 747
🏁 Script executed:
rg -n -B2 -A5 'metadata:' web/sdk/src/types.tsRepository: raystack/frontier
Length of output: 380
🏁 Script executed:
fd -e ts -e tsx web/sdk/react/views-new/plans/components/feature-table.tsx -x cat -nRepository: raystack/frontier
Length of output: 43
🏁 Script executed:
rg -n 'metadata\s*=' web/sdk/src --type ts --type tsx -A2 -B2Repository: raystack/frontier
Length of output: 88
🏁 Script executed:
rg -n 'productNames' web/sdk/react/views-new/plans --type ts -B5 -A5Repository: raystack/frontier
Length of output: 1184
🏁 Script executed:
rg -n 'planFeature\|features\[' web/sdk/react/views-new/plans/components/feature-table.tsx -B3 -A3Repository: raystack/frontier
Length of output: 43
🏁 Script executed:
rg -n 'interface.*Feature|type.*Feature' web/sdk/src/types.ts -B1 -A8Repository: raystack/frontier
Length of output: 43
🏁 Script executed:
cat -n web/sdk/react/views-new/plans/components/feature-table.tsxRepository: raystack/frontier
Length of output: 3151
🏁 Script executed:
cat -n web/sdk/src/types.ts | head -100Repository: raystack/frontier
Length of output: 2446
🏁 Script executed:
rg -n 'IntervalPricingWithPlan|interface.*Plan' web/sdk/src/types.ts -A10Repository: raystack/frontier
Length of output: 858
🏁 Script executed:
rg -n 'Feature.*metadata|get.*planFeature' web/sdk/react/views-new/plans -B2 -A5Repository: raystack/frontier
Length of output: 1097
🏁 Script executed:
rg -n '@raystack/proton' web/sdk/src/types.tsRepository: raystack/frontier
Length of output: 121
🏁 Script executed:
rg -n 'Record<string, Feature>' web/sdk/src/types.ts -B3 -A3Repository: raystack/frontier
Length of output: 234
🏁 Script executed:
rg -n 'export.*Feature\|interface.*Feature' web/sdk/react --type ts -B2 -A8Repository: raystack/frontier
Length of output: 43
🏁 Script executed:
find web/sdk -name "*.ts" -o -name "*.tsx" | xargs grep -l "Feature.*metadata\|metadata.*Record" | head -5Repository: raystack/frontier
Length of output: 379
🏁 Script executed:
rg -n 'import.*Feature' web/sdk/react/views-new/plans/components/feature-table.tsx -B2 -A2Repository: raystack/frontier
Length of output: 43
🏁 Script executed:
rg -n 'Feature' web/sdk/src/types.ts | grep -i importRepository: raystack/frontier
Length of output: 121
🏁 Script executed:
rg -n 'as Record<string, number>' web/sdk/react/views-new/plans -B3 -A3Repository: raystack/frontier
Length of output: 1803
Normalize metadata values to strings before calling .toLowerCase() to prevent runtime errors with numeric metadata.
At Line 25, value?.toLowerCase() assumes a string, but metadata values can be numeric (as evidenced by type assertions elsewhere in plans-view.tsx and confirm-plan-change-dialog.tsx casting metadata as Record<string, number>). Additionally, Line 24 uses || which incorrectly replaces valid falsy values like 0 and false with '-'.
Replace the || operator with nullish coalescing (??) and explicitly convert to string:
- const value = productMetaDataFeatureValues?.[0] || '-';
- const isAvailable = value?.toLowerCase() === 'true';
+ const rawValue = productMetaDataFeatureValues?.[0] ?? '-';
+ const displayValue = rawValue == null ? '-' : String(rawValue);
+ const isAvailable = displayValue.toLowerCase() === 'true';Update the render at Line 37:
- {value}
+ {displayValue}| const onPlanActionClick = useCallback(() => { | ||
| onConfirmPlanChange?.({ | ||
| planId: selectedIntervalPricing?.planId, | ||
| amount: selectedIntervalPricing?.amount, | ||
| currency: selectedIntervalPricing?.currency | ||
| }); | ||
| }, [onConfirmPlanChange, selectedIntervalPricing]); |
There was a problem hiding this comment.
planId could be undefined when invoking callback.
If selectedIntervalPricing is undefined, planId will be undefined. The ConfirmPlanChangeDialog expects a valid planId string.
Consider guarding the callback invocation:
🛡️ Suggested fix
const onPlanActionClick = useCallback(() => {
+ if (!selectedIntervalPricing?.planId) return;
onConfirmPlanChange?.({
planId: selectedIntervalPricing?.planId,
amount: selectedIntervalPricing?.amount,
currency: selectedIntervalPricing?.currency
});
}, [onConfirmPlanChange, selectedIntervalPricing]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const onPlanActionClick = useCallback(() => { | |
| onConfirmPlanChange?.({ | |
| planId: selectedIntervalPricing?.planId, | |
| amount: selectedIntervalPricing?.amount, | |
| currency: selectedIntervalPricing?.currency | |
| }); | |
| }, [onConfirmPlanChange, selectedIntervalPricing]); | |
| const onPlanActionClick = useCallback(() => { | |
| if (!selectedIntervalPricing?.planId) return; | |
| onConfirmPlanChange?.({ | |
| planId: selectedIntervalPricing?.planId, | |
| amount: selectedIntervalPricing?.amount, | |
| currency: selectedIntervalPricing?.currency | |
| }); | |
| }, [onConfirmPlanChange, selectedIntervalPricing]); |
Summary
views/planstoviews-new/plansusing@raystack/apsara-v1components, CSS Modules, and design tokensusePlans,useFrontier,groupPlansPricingByInterval,listFeaturesquery) without modification