Skip to content

Commit 3fbaf11

Browse files
committed
fix(mobile): align ios subscription upgrade actions
1 parent 3947302 commit 3fbaf11

File tree

1 file changed

+179
-39
lines changed
  • apps/mobile/src/modules/settings/routes

1 file changed

+179
-39
lines changed

apps/mobile/src/modules/settings/routes/Plan.tsx

Lines changed: 179 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,117 @@ const isFeatureValueVisible = (value: PaymentFeature[keyof PaymentFeature] | nul
192192

193193
const isAppleProductId = (value: string | undefined): value is string => typeof value === "string"
194194

195+
const BILLING_PERIOD_RANK: Record<BillingPeriod, number> = {
196+
monthly: 0,
197+
yearly: 1,
198+
}
199+
200+
type PlanActionType = "current" | "upgrade" | "new" | "coming-soon" | null
201+
202+
const matchesPlanIdentifier = (
203+
plan: PaymentPlan,
204+
identifier: string | null | undefined,
205+
): boolean => {
206+
if (!identifier) {
207+
return false
208+
}
209+
210+
return plan.planID === identifier || plan.role === identifier
211+
}
212+
213+
const getAppleProductIdForBillingPeriod = (
214+
plan: PaymentPlan,
215+
billingPeriod: BillingPeriod,
216+
): string | null => {
217+
const productId =
218+
billingPeriod === "yearly" ? plan.appleProductIdentifierAnnual : plan.appleProductIdentifier
219+
220+
return productId ?? null
221+
}
222+
223+
const inferBillingPeriodFromProductId = (
224+
productId: string | null | undefined,
225+
plan: PaymentPlan,
226+
): BillingPeriod | null => {
227+
if (!productId) {
228+
return null
229+
}
230+
231+
if (productId === plan.appleProductIdentifierAnnual) {
232+
return "yearly"
233+
}
234+
235+
if (productId === plan.appleProductIdentifier) {
236+
return "monthly"
237+
}
238+
239+
const normalizedProductId = productId.toLowerCase()
240+
241+
if (normalizedProductId.includes("annual") || normalizedProductId.includes("year")) {
242+
return "yearly"
243+
}
244+
245+
if (normalizedProductId.includes("month")) {
246+
return "monthly"
247+
}
248+
249+
return null
250+
}
251+
252+
const getPlanActionType = ({
253+
plan,
254+
billingPeriod,
255+
currentPlan,
256+
activeSubscription,
257+
hasExistingSubscription,
258+
}: {
259+
plan: PaymentPlan
260+
billingPeriod: BillingPeriod
261+
currentPlan: PaymentPlan | null
262+
activeSubscription?: ActiveSubscription
263+
hasExistingSubscription: boolean
264+
}): PlanActionType => {
265+
if (plan.isComingSoon) {
266+
return "coming-soon"
267+
}
268+
269+
const hasCheckout = !!plan.planID
270+
const targetTier = plan.tier ?? 0
271+
const currentTier = currentPlan?.tier ?? 0
272+
const isSamePlan =
273+
matchesPlanIdentifier(plan, activeSubscription?.plan) ||
274+
(!!currentPlan && plan.role === currentPlan.role)
275+
276+
if (isSamePlan) {
277+
if (!hasExistingSubscription) {
278+
return "current"
279+
}
280+
281+
const currentBillingPeriod = inferBillingPeriodFromProductId(
282+
activeSubscription?.productId,
283+
plan,
284+
)
285+
if (
286+
currentBillingPeriod &&
287+
BILLING_PERIOD_RANK[billingPeriod] > BILLING_PERIOD_RANK[currentBillingPeriod]
288+
) {
289+
return "upgrade"
290+
}
291+
292+
return "current"
293+
}
294+
295+
if (!hasCheckout) {
296+
return null
297+
}
298+
299+
if (!hasExistingSubscription) {
300+
return currentTier === 0 ? "new" : targetTier > currentTier ? "upgrade" : null
301+
}
302+
303+
return targetTier > currentTier ? "upgrade" : null
304+
}
305+
195306
export const PlanScreen: NavigationControllerView = () => {
196307
const { t } = useTranslation("settings")
197308
const serverConfigs = useServerConfigs()
@@ -245,6 +356,42 @@ export const PlanScreen: NavigationControllerView = () => {
245356
return sortedPlans.find((plan) => plan.role === role) ?? null
246357
}, [sortedPlans, role])
247358

359+
const billingSubscriptionQuery = useQuery({
360+
queryKey: ["billingSubscription"],
361+
queryFn: async () => {
362+
const response = await followClient.request<{ code: number; data: ActiveSubscription }>(
363+
"/billing/subscription",
364+
)
365+
return response.data
366+
},
367+
enabled: !!whoami?.id,
368+
})
369+
370+
const activeSubscription = billingSubscriptionQuery.data
371+
const subscribedPlan = useMemo(() => {
372+
if (!activeSubscription?.plan) {
373+
return currentPlan
374+
}
375+
376+
return (
377+
sortedPlans.find((plan) => matchesPlanIdentifier(plan, activeSubscription.plan)) ??
378+
currentPlan
379+
)
380+
}, [activeSubscription?.plan, currentPlan, sortedPlans])
381+
382+
const hasExistingSubscription = useMemo(() => {
383+
if (activeSubscription) {
384+
return Boolean(
385+
activeSubscription.plan ||
386+
activeSubscription.source ||
387+
activeSubscription.productId ||
388+
activeSubscription.status,
389+
)
390+
}
391+
392+
return role != null && role !== UserRole.Free
393+
}, [activeSubscription, role])
394+
248395
const daysLeft = useMemo(() => {
249396
if (!roleEndAt) {
250397
return null
@@ -278,8 +425,9 @@ export const PlanScreen: NavigationControllerView = () => {
278425

279426
const upgradeMutation = useMutation<void, Error, UpgradeVariables>({
280427
mutationFn: async ({ planId, annual }) => {
281-
if (Platform.OS === "ios") {
282-
const selectedPlan = plans.find((plan) => plan.planID === planId)
428+
const selectedPlan = plans.find((plan) => plan.planID === planId)
429+
430+
if (Platform.OS === "ios" && activeSubscription?.source !== "stripe") {
283431
const productId = annual
284432
? selectedPlan?.appleProductIdentifierAnnual
285433
: selectedPlan?.appleProductIdentifier
@@ -318,17 +466,6 @@ export const PlanScreen: NavigationControllerView = () => {
318466
},
319467
})
320468

321-
const billingSubscriptionQuery = useQuery({
322-
queryKey: ["billingSubscription"],
323-
queryFn: async () => {
324-
const response = await followClient.request<{ code: number; data: ActiveSubscription }>(
325-
"/billing/subscription",
326-
)
327-
return response.data
328-
},
329-
enabled: !!whoami?.id,
330-
})
331-
332469
const billingPortalMutation = useMutation({
333470
mutationFn: async () => {
334471
const data = await followClient.request<{ code: number; data?: { url: string } }>(
@@ -348,15 +485,15 @@ export const PlanScreen: NavigationControllerView = () => {
348485
})
349486

350487
const handleManageSubscription = useCallback(() => {
351-
if (billingSubscriptionQuery.data?.source === "apple") {
488+
if (activeSubscription?.source === "apple") {
352489
void openSubscriptionManagement().catch(() => {
353490
toast.error(t("subscription.actions.manage_error"))
354491
})
355492
return
356493
}
357494

358495
billingPortalMutation.mutate()
359-
}, [billingPortalMutation, billingSubscriptionQuery.data?.source, openSubscriptionManagement, t])
496+
}, [activeSubscription?.source, billingPortalMutation, openSubscriptionManagement, t])
360497

361498
if (!isPaymentEnabled || sortedPlans.length === 0) {
362499
return (
@@ -378,8 +515,9 @@ export const PlanScreen: NavigationControllerView = () => {
378515
)
379516
}
380517

381-
const summaryTitle = currentPlan
382-
? t("subscription.summary.current", { plan: currentPlan.name })
518+
const summaryPlan = subscribedPlan ?? currentPlan
519+
const summaryTitle = summaryPlan
520+
? t("subscription.summary.current", { plan: summaryPlan.name })
383521
: t("subscription.summary.free")
384522

385523
let summarySubtitle = t("subscription.summary.free_description")
@@ -388,7 +526,7 @@ export const PlanScreen: NavigationControllerView = () => {
388526
date: dayjs(roleEndAt).format("MMMM D, YYYY"),
389527
days: daysLeft,
390528
})
391-
} else if (currentPlan && currentPlan.role !== UserRole.Free) {
529+
} else if (summaryPlan && summaryPlan.role !== UserRole.Free) {
392530
summarySubtitle = t("subscription.summary.active")
393531
}
394532

@@ -421,27 +559,38 @@ export const PlanScreen: NavigationControllerView = () => {
421559

422560
<View className="gap-4">
423561
{sortedPlans.map((plan) => {
424-
const isCurrentPlan = plan.role === role
562+
const isCurrentPlan =
563+
matchesPlanIdentifier(plan, activeSubscription?.plan) || plan.role === role
425564
const isProcessing =
426565
upgradeMutation.isPending && upgradeMutation.variables?.planId === plan.planID
566+
const actionType = getPlanActionType({
567+
plan,
568+
billingPeriod,
569+
currentPlan: subscribedPlan ?? currentPlan,
570+
activeSubscription,
571+
hasExistingSubscription,
572+
})
427573

428574
return (
429575
<PlanCard
430576
key={plan.planID ?? plan.name}
431577
plan={plan}
432578
billingPeriod={billingPeriod}
579+
actionType={actionType}
433580
isCurrentPlan={isCurrentPlan}
434581
storeProduct={
435582
Platform.OS === "ios"
436583
? appleProductsById.get(
437-
billingPeriod === "yearly"
438-
? (plan.appleProductIdentifierAnnual ?? "")
439-
: (plan.appleProductIdentifier ?? ""),
584+
getAppleProductIdForBillingPeriod(plan, billingPeriod) ?? "",
440585
)
441586
: undefined
442587
}
443588
onUpgrade={
444-
plan.planID
589+
plan.planID &&
590+
actionType &&
591+
actionType !== "current" &&
592+
actionType !== "coming-soon" &&
593+
(!hasExistingSubscription || billingSubscriptionQuery.isFetched)
445594
? () =>
446595
upgradeMutation.mutate({
447596
planId: plan.planID as string,
@@ -608,6 +757,7 @@ const BillingToggle = ({
608757
const PlanCard = ({
609758
plan,
610759
billingPeriod,
760+
actionType,
611761
isCurrentPlan,
612762
storeProduct,
613763
onUpgrade,
@@ -619,6 +769,7 @@ const PlanCard = ({
619769
}: {
620770
plan: PaymentPlan
621771
billingPeriod: BillingPeriod
772+
actionType: PlanActionType
622773
isCurrentPlan: boolean
623774
storeProduct?: { displayPrice?: string } | null
624775
onUpgrade?: () => void
@@ -706,19 +857,6 @@ const PlanCard = ({
706857
.slice(0, 6)
707858
}, [plan.limit])
708859

709-
const actionType = useMemo(() => {
710-
if (plan.isComingSoon) {
711-
return "coming-soon" as const
712-
}
713-
if (isCurrentPlan) {
714-
return "current" as const
715-
}
716-
if (plan.planID) {
717-
return "upgrade" as const
718-
}
719-
return null
720-
}, [plan.isComingSoon, plan.planID, isCurrentPlan])
721-
722860
const planNameFallback = plan.name || (plan.role ? UserRoleName[plan.role as UserRole] : "")
723861

724862
return (
@@ -822,7 +960,7 @@ const PlanAction = ({
822960
activeSubscription,
823961
upgradeButtonText,
824962
}: {
825-
actionType: "current" | "upgrade" | "coming-soon" | null
963+
actionType: "current" | "upgrade" | "new" | "coming-soon" | null
826964
onUpgrade?: () => void
827965
onManageSubscription?: () => void
828966
isProcessing?: boolean
@@ -908,7 +1046,7 @@ const PlanAction = ({
9081046
)
9091047
}
9101048

911-
if (actionType === "upgrade" && onUpgrade) {
1049+
if ((actionType === "upgrade" || actionType === "new") && onUpgrade) {
9121050
return (
9131051
<Pressable
9141052
accessibilityRole="button"
@@ -920,7 +1058,9 @@ const PlanAction = ({
9201058
<ActivityIndicator color="white" />
9211059
) : (
9221060
<Text className="text-base font-semibold text-white">
923-
{upgradeButtonText || t("subscription.actions.upgrade")}
1061+
{actionType === "new"
1062+
? (upgradeButtonText ?? t("subscription.actions.upgrade"))
1063+
: t("subscription.actions.upgrade")}
9241064
</Text>
9251065
)}
9261066
</Pressable>

0 commit comments

Comments
 (0)