11/**
22 * Billing Page Component
3- *
3+ *
44 * Matches web's "Billing Status – Manage your credits and subscription" design
55 */
66
@@ -18,6 +18,9 @@ import {
1818 useSubscriptionCommitment ,
1919 useScheduledChanges ,
2020 billingKeys ,
21+ presentCustomerInfo ,
22+ shouldUseRevenueCat ,
23+ isRevenueCatConfigured ,
2124} from '@/lib/billing' ;
2225import { useAuthContext } from '@/contexts' ;
2326import { useLanguage } from '@/contexts' ;
@@ -40,6 +43,7 @@ import {
4043 CreditCard ,
4144 AlertCircle ,
4245 ArrowRight ,
46+ Settings ,
4347} from 'lucide-react-native' ;
4448import { formatCredits } from '@/lib/utils/credit-formatter' ;
4549import { ScheduledDowngradeCard } from '@/components/billing/ScheduledDowngradeCard' ;
@@ -104,8 +108,8 @@ export function BillingPage({ visible, onClose, onChangePlan }: BillingPageProps
104108 Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Light ) ;
105109 try {
106110 // Use kortix.com for production, staging.suna.so for staging
107- const baseUrl = process . env . EXPO_PUBLIC_ENV === 'staging'
108- ? 'https://staging.suna.so'
111+ const baseUrl = process . env . EXPO_PUBLIC_ENV === 'staging'
112+ ? 'https://staging.suna.so'
109113 : 'https://www.kortix.com' ;
110114 await WebBrowser . openBrowserAsync ( `${ baseUrl } /credits-explained` , {
111115 presentationStyle : WebBrowser . WebBrowserPresentationStyle . PAGE_SHEET ,
@@ -118,6 +122,7 @@ export function BillingPage({ visible, onClose, onChangePlan }: BillingPageProps
118122 const creditsButtonScale = useSharedValue ( 1 ) ;
119123 const creditsLinkScale = useSharedValue ( 1 ) ;
120124 const changePlanButtonScale = useSharedValue ( 1 ) ;
125+ const customerInfoButtonScale = useSharedValue ( 1 ) ;
121126
122127 const creditsButtonStyle = useAnimatedStyle ( ( ) => ( {
123128 transform : [ { scale : creditsButtonScale . value } ] ,
@@ -131,11 +136,29 @@ export function BillingPage({ visible, onClose, onChangePlan }: BillingPageProps
131136 transform : [ { scale : changePlanButtonScale . value } ] ,
132137 } ) ) ;
133138
139+ const customerInfoButtonStyle = useAnimatedStyle ( ( ) => ( {
140+ transform : [ { scale : customerInfoButtonScale . value } ] ,
141+ } ) ) ;
142+
134143 const handleChangePlan = useCallback ( ( ) => {
135144 Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Light ) ;
136145 onChangePlan ?.( ) ;
137146 } , [ onChangePlan ] ) ;
138147
148+ const handleCustomerInfo = useCallback ( async ( ) => {
149+ Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Light ) ;
150+ try {
151+ await presentCustomerInfo ( ) ;
152+ // Refresh billing data after user returns from customer info portal
153+ handleSubscriptionUpdate ( ) ;
154+ } catch ( error ) {
155+ console . error ( 'Error presenting customer info portal:' , error ) ;
156+ Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Error ) ;
157+ }
158+ } , [ handleSubscriptionUpdate ] ) ;
159+
160+ const useRevenueCat = shouldUseRevenueCat ( ) && isRevenueCatConfigured ( ) ;
161+
139162 if ( ! visible ) return null ;
140163
141164 if ( isLoadingSubscription ) {
@@ -180,14 +203,14 @@ export function BillingPage({ visible, onClose, onChangePlan }: BillingPageProps
180203 const monthlyCredits = credits ?. monthly || 0 ;
181204 const extraCredits = credits ?. extra || 0 ;
182205 const dailyRefreshInfo = credits ?. daily_refresh ;
183-
206+
184207 // Calculate refresh time for daily credits
185208 const getDailyRefreshTime = ( ) : string | null => {
186209 if ( ! dailyRefreshInfo ?. enabled ) return null ;
187-
210+
188211 let hours : number ;
189212 let seconds : number | undefined ;
190-
213+
191214 if ( dailyRefreshInfo . seconds_until_refresh ) {
192215 seconds = dailyRefreshInfo . seconds_until_refresh ;
193216 hours = Math . ceil ( seconds / 3600 ) ;
@@ -201,25 +224,25 @@ export function BillingPage({ visible, onClose, onChangePlan }: BillingPageProps
201224 console . log ( '⚠️ No refresh info available:' , dailyRefreshInfo ) ;
202225 return null ; // No refresh info available
203226 }
204-
227+
205228 // Debug logging
206229 console . log ( '🕐 Daily refresh calculation:' , {
207230 seconds_until_refresh : dailyRefreshInfo . seconds_until_refresh ,
208231 next_refresh_at : dailyRefreshInfo . next_refresh_at ,
209232 calculatedSeconds : seconds ,
210233 calculatedHours : hours ,
211234 } ) ;
212-
235+
213236 // Handle edge cases
214237 if ( hours <= 0 || isNaN ( hours ) ) {
215238 console . log ( '⚠️ Invalid hours:' , hours ) ;
216239 return null ; // Invalid or past refresh time
217240 }
218-
241+
219242 if ( hours === 1 ) {
220243 return t ( 'billing.refreshIn1Hour' , 'Refresh in 1 hour' ) ;
221244 }
222-
245+
223246 // Show actual hours
224247 return `Refresh in ${ hours } h` ;
225248 } ;
@@ -237,7 +260,7 @@ export function BillingPage({ visible, onClose, onChangePlan }: BillingPageProps
237260 // Calculate next billing date - matches frontend formatDateFlexible
238261 const getNextBillingDate = ( ) : string | null => {
239262 if ( ! accountState ?. subscription ?. current_period_end ) return null ;
240-
263+
241264 const formatDateFlexible = ( dateValue : string | number ) : string => {
242265 if ( typeof dateValue === 'number' ) {
243266 // Unix timestamp in seconds - convert to milliseconds
@@ -254,12 +277,12 @@ export function BillingPage({ visible, onClose, onChangePlan }: BillingPageProps
254277 day : 'numeric' ,
255278 } ) ;
256279 } ;
257-
280+
258281 return formatDateFlexible ( accountState . subscription . current_period_end ) ;
259282 } ;
260283
261284 const nextBillingDate = getNextBillingDate ( ) ;
262-
285+
263286 const dailyRefreshTime = getDailyRefreshTime ( ) ;
264287 const monthlyRefreshTime = getMonthlyRefreshTime ( ) ;
265288 const hasCommitment = commitmentData ?. has_commitment ;
@@ -420,9 +443,9 @@ export function BillingPage({ visible, onClose, onChangePlan }: BillingPageProps
420443 < Text className = "text-sm text-muted-foreground" >
421444 { t ( 'billing.currentPlan' , 'Current Plan' ) }
422445 </ Text >
423- < PricingTierBadge
424- planName = { accountState ?. subscription ?. tier_display_name || accountState ?. subscription ?. tier_key || 'Basic' }
425- size = "lg"
446+ < PricingTierBadge
447+ planName = { accountState ?. subscription ?. tier_display_name || accountState ?. subscription ?. tier_key || 'Basic' }
448+ size = "lg"
426449 />
427450 </ View >
428451
@@ -533,6 +556,26 @@ export function BillingPage({ visible, onClose, onChangePlan }: BillingPageProps
533556 </ AnimatedPressable >
534557 ) }
535558
559+ { /* RevenueCat Customer Info Portal */ }
560+ { useRevenueCat && (
561+ < AnimatedPressable
562+ onPress = { handleCustomerInfo }
563+ onPressIn = { ( ) => {
564+ customerInfoButtonScale . value = withSpring ( 0.96 , { damping : 15 , stiffness : 400 } ) ;
565+ } }
566+ onPressOut = { ( ) => {
567+ customerInfoButtonScale . value = withSpring ( 1 , { damping : 15 , stiffness : 400 } ) ;
568+ } }
569+ style = { customerInfoButtonStyle }
570+ className = "w-full h-12 bg-card border border-border rounded-2xl items-center justify-center flex-row gap-2"
571+ >
572+ < Icon as = { Settings } size = { 18 } className = "text-foreground" strokeWidth = { 2 } />
573+ < Text className = "text-sm font-roobert-semibold text-foreground" >
574+ { t ( 'billing.customerInfo' , 'Customer Info' ) }
575+ </ Text >
576+ </ AnimatedPressable >
577+ ) }
578+
536579 </ View >
537580 </ AnimatedView >
538581
0 commit comments