@@ -192,6 +192,117 @@ const isFeatureValueVisible = (value: PaymentFeature[keyof PaymentFeature] | nul
192192
193193const 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+
195306export 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 = ({
608757const 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