Skip to content

Commit 0031071

Browse files
authored
Merge pull request #2318 from kortix-ai/feature/mobile-upgrades
Feature/mobile upgrades
2 parents d06726b + 5d7903b commit 0031071

File tree

4 files changed

+86
-21
lines changed

4 files changed

+86
-21
lines changed

apps/mobile/components/settings/BillingPage.tsx

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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';
2225
import { useAuthContext } from '@/contexts';
2326
import { useLanguage } from '@/contexts';
@@ -40,6 +43,7 @@ import {
4043
CreditCard,
4144
AlertCircle,
4245
ArrowRight,
46+
Settings,
4347
} from 'lucide-react-native';
4448
import { formatCredits } from '@/lib/utils/credit-formatter';
4549
import { 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

apps/mobile/lib/billing/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// CORE EXPORTS - Unified Account State
33
// =============================================================================
44
export { billingApi, accountStateSelectors, type AccountState } from './api';
5-
export {
5+
export {
66
useAccountState,
77
useAccountStateWithStreaming,
88
accountStateKeys,
@@ -85,6 +85,7 @@ export {
8585
getCustomerInfo,
8686
checkSubscriptionStatus,
8787
presentPaywall,
88+
presentCustomerInfo,
8889
} from './revenuecat';
8990
export type { RevenueCatProduct } from './revenuecat';
9091

@@ -95,11 +96,11 @@ export { getPlanName, getPlanIcon } from './plan-utils';
9596

9697
export { logAvailableProducts, findPackageForTier } from './revenuecat-utils';
9798
export { debugRevenueCat } from './debug-revenuecat';
98-
export {
99-
getRevenueCatPricing,
100-
getRevenueCatDisplayPrice,
99+
export {
100+
getRevenueCatPricing,
101+
getRevenueCatDisplayPrice,
101102
getRevenueCatPackageForCheckout,
102103
getRevenueCatYearlySavings,
103-
type RevenueCatPricingData
104+
type RevenueCatPricingData
104105
} from './revenuecat-pricing';
105106

apps/mobile/lib/billing/revenuecat.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,3 +653,23 @@ export async function presentPaywall(
653653
throw error;
654654
}
655655
}
656+
657+
/**
658+
* Present RevenueCat Customer Info Portal
659+
*
660+
* Shows the native RevenueCat customer info screen where users can:
661+
* - View subscription details
662+
* - Manage payment methods
663+
* - View purchase history
664+
* - Restore purchases
665+
*/
666+
export async function presentCustomerInfo(): Promise<void> {
667+
try {
668+
console.log('📱 Presenting RevenueCat customer info portal...');
669+
await RevenueCatUI.presentCustomerCenter();
670+
console.log('✅ Customer info portal dismissed');
671+
} catch (error) {
672+
console.error('❌ Error presenting customer info portal:', error);
673+
throw error;
674+
}
675+
}

apps/mobile/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@
600600
"howCreditsWork": "How credits work",
601601
"learnAboutCredits": "Learn about credit usage",
602602
"cancelPlan": "Cancel Plan",
603+
"customerInfo": "Customer Info",
603604
"usage": "Usage",
604605
"last30Days": "Last 30d",
605606
"failedToLoad": "Failed to load subscription data",

0 commit comments

Comments
 (0)