@umituz/react-native-subscription 2.27.123 → 2.27.124
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/credits/application/creditOperationUtils.ts +65 -144
- package/src/domains/credits/application/creditOperationUtils.types.ts +19 -0
- package/src/domains/credits/presentation/useCredits.ts +1 -11
- package/src/domains/paywall/components/PaywallModal.tsx +14 -107
- package/src/domains/paywall/components/PaywallModal.types.ts +26 -0
- package/src/domains/paywall/components/PlanCard.tsx +45 -148
- package/src/domains/paywall/components/PlanCard.types.ts +12 -0
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +116 -0
- package/src/domains/subscription/application/SubscriptionSyncService.ts +10 -96
- package/src/domains/subscription/application/SubscriptionSyncUtils.ts +0 -2
- package/src/domains/subscription/core/SubscriptionConstants.ts +1 -13
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +7 -13
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.types.ts +15 -0
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +20 -5
- package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.tsx +13 -92
- package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.types.ts +47 -0
- package/src/domains/subscription/presentation/screens/components/UpgradePrompt.tsx +34 -126
- package/src/domains/subscription/presentation/screens/components/UpgradePrompt.types.ts +12 -0
- package/src/domains/subscription/presentation/usePremium.ts +3 -22
- package/src/domains/subscription/presentation/usePremium.types.ts +16 -0
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +30 -22
- package/src/domains/subscription/presentation/useSubscriptionStatus.types.ts +7 -0
- package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +3 -16
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +1 -13
- package/src/domains/wallet/presentation/screens/WalletScreen.tsx +25 -112
- package/src/domains/wallet/presentation/screens/WalletScreen.types.ts +15 -0
- package/src/shared/utils/appValidators.ts +38 -0
- package/src/shared/utils/validators.ts +4 -122
- package/src/domains/paywall/components/README.md +0 -41
- package/src/domains/subscription/presentation/screens/README.md +0 -52
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.27.
|
|
3
|
+
"version": "2.27.124",
|
|
4
4
|
"description": "Complete subscription management with RevenueCat, paywall UI, and credits system for React Native apps",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -1,154 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
* Credit Operation Utilities
|
|
3
|
-
* Business logic for credit calculations and data building
|
|
4
|
-
*/
|
|
5
|
-
|
|
1
|
+
import { Timestamp, serverTimestamp } from "firebase/firestore";
|
|
6
2
|
import { resolveSubscriptionStatus } from "../../subscription/core/SubscriptionStatus";
|
|
7
3
|
import { creditAllocationOrchestrator } from "./credit-strategies/CreditAllocationOrchestrator";
|
|
8
4
|
import { isPast } from "../../../utils";
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
5
|
+
import {
|
|
6
|
+
CalculateCreditsParams,
|
|
7
|
+
BuildCreditsDataParams
|
|
8
|
+
} from "./creditOperationUtils.types";
|
|
9
|
+
|
|
10
|
+
export function calculateNewCredits({ metadata, existingData, creditLimit, purchaseId }: CalculateCreditsParams): number {
|
|
11
|
+
const isPremium = metadata.isPremium;
|
|
12
|
+
const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
|
|
13
|
+
const status = resolveSubscriptionStatus({
|
|
14
|
+
isPremium,
|
|
15
|
+
willRenew: metadata.willRenew ?? false,
|
|
16
|
+
isExpired,
|
|
17
|
+
periodType: metadata.periodType ?? undefined,
|
|
18
|
+
});
|
|
19
|
+
const isStatusSync = purchaseId.startsWith("status_sync_");
|
|
20
|
+
return creditAllocationOrchestrator.allocate({
|
|
21
|
+
status,
|
|
22
|
+
isStatusSync,
|
|
23
|
+
existingData,
|
|
24
|
+
creditLimit,
|
|
25
|
+
isSubscriptionActive: isPremium && !isExpired,
|
|
26
|
+
productId: metadata.productId,
|
|
27
|
+
});
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* Build credits data object for Firestore update
|
|
61
|
-
*/
|
|
62
|
-
export function buildCreditsData(params: BuildCreditsDataParams): Record<string, any> {
|
|
63
|
-
const {
|
|
64
|
-
existingData,
|
|
65
|
-
newCredits,
|
|
66
|
-
creditLimit,
|
|
67
|
-
purchaseId,
|
|
68
|
-
metadata,
|
|
69
|
-
purchaseHistory,
|
|
70
|
-
platform,
|
|
71
|
-
} = params;
|
|
72
|
-
|
|
73
|
-
const isPremium = metadata.isPremium;
|
|
74
|
-
const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
|
|
75
|
-
|
|
76
|
-
const status = resolveSubscriptionStatus({
|
|
77
|
-
isPremium,
|
|
78
|
-
willRenew: metadata.willRenew ?? false,
|
|
79
|
-
isExpired,
|
|
80
|
-
periodType: metadata.periodType ?? undefined,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const newProcessedPurchases = addProcessedPurchase(existingData.processedPurchases, purchaseId);
|
|
84
|
-
|
|
85
|
-
const creditsData: Record<string, any> = {
|
|
86
|
-
isPremium,
|
|
87
|
-
status,
|
|
88
|
-
credits: newCredits,
|
|
89
|
-
creditLimit,
|
|
90
|
-
lastUpdatedAt: serverTimestamp(),
|
|
91
|
-
processedPurchases: newProcessedPurchases,
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
if (purchaseHistory.length > 0) {
|
|
95
|
-
creditsData.purchaseHistory = purchaseHistory;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const isNewPurchaseOrRenewal = purchaseId.startsWith("purchase_") || purchaseId.startsWith("renewal_");
|
|
99
|
-
if (isNewPurchaseOrRenewal) {
|
|
100
|
-
creditsData.lastPurchaseAt = serverTimestamp();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (metadata.expirationDate) {
|
|
104
|
-
creditsData.expirationDate = serverTimestamp();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (metadata.willRenew !== undefined) {
|
|
108
|
-
creditsData.willRenew = metadata.willRenew;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (metadata.originalTransactionId) {
|
|
112
|
-
creditsData.originalTransactionId = metadata.originalTransactionId;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
creditsData.productId = metadata.productId;
|
|
116
|
-
creditsData.platform = platform;
|
|
117
|
-
|
|
118
|
-
return creditsData;
|
|
30
|
+
export function buildCreditsData({
|
|
31
|
+
existingData, newCredits, creditLimit, purchaseId, metadata, purchaseHistory, platform
|
|
32
|
+
}: BuildCreditsDataParams): Record<string, any> {
|
|
33
|
+
const isPremium = metadata.isPremium;
|
|
34
|
+
const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
|
|
35
|
+
const status = resolveSubscriptionStatus({
|
|
36
|
+
isPremium,
|
|
37
|
+
willRenew: metadata.willRenew ?? false,
|
|
38
|
+
isExpired,
|
|
39
|
+
periodType: metadata.periodType ?? undefined,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const creditsData: Record<string, any> = {
|
|
43
|
+
isPremium,
|
|
44
|
+
status,
|
|
45
|
+
credits: newCredits,
|
|
46
|
+
creditLimit,
|
|
47
|
+
lastUpdatedAt: serverTimestamp(),
|
|
48
|
+
processedPurchases: [...(existingData?.processedPurchases ?? []), purchaseId].slice(-50),
|
|
49
|
+
productId: metadata.productId,
|
|
50
|
+
platform,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (purchaseHistory.length > 0) creditsData.purchaseHistory = purchaseHistory;
|
|
54
|
+
if (purchaseId.startsWith("purchase_") || purchaseId.startsWith("renewal_")) creditsData.lastPurchaseAt = serverTimestamp();
|
|
55
|
+
if (metadata.expirationDate) creditsData.expirationDate = Timestamp.fromDate(new Date(metadata.expirationDate));
|
|
56
|
+
if (metadata.willRenew !== undefined) creditsData.willRenew = metadata.willRenew;
|
|
57
|
+
if (metadata.originalTransactionId) creditsData.originalTransactionId = metadata.originalTransactionId;
|
|
58
|
+
|
|
59
|
+
return creditsData;
|
|
119
60
|
}
|
|
120
61
|
|
|
121
|
-
/**
|
|
122
|
-
* Check if status sync write should be skipped (no changes)
|
|
123
|
-
*/
|
|
124
62
|
export function shouldSkipStatusSyncWrite(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
63
|
+
purchaseId: string,
|
|
64
|
+
existingData: any,
|
|
65
|
+
newCreditsData: Record<string, any>
|
|
128
66
|
): boolean {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
existingData.status !== newCreditsData.status ||
|
|
138
|
-
existingData.credits !== newCreditsData.credits ||
|
|
139
|
-
existingData.creditLimit !== newCreditsData.creditLimit ||
|
|
140
|
-
existingData.productId !== newCreditsData.productId;
|
|
141
|
-
|
|
142
|
-
return !hasChanged;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Add purchase ID to processed purchases list
|
|
147
|
-
*/
|
|
148
|
-
function addProcessedPurchase(
|
|
149
|
-
existing: string[],
|
|
150
|
-
purchaseId: string,
|
|
151
|
-
limit: number = 50
|
|
152
|
-
): string[] {
|
|
153
|
-
return [...existing, purchaseId].slice(-limit);
|
|
67
|
+
if (!purchaseId.startsWith("status_sync_")) return false;
|
|
68
|
+
return (
|
|
69
|
+
existingData.isPremium === newCreditsData.isPremium &&
|
|
70
|
+
existingData.status === newCreditsData.status &&
|
|
71
|
+
existingData.credits === newCreditsData.credits &&
|
|
72
|
+
existingData.creditLimit === newCreditsData.creditLimit &&
|
|
73
|
+
existingData.productId === newCreditsData.productId
|
|
74
|
+
);
|
|
154
75
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
|
|
2
|
+
import type { InitializeCreditsMetadata } from "../../subscription/application/SubscriptionInitializerTypes";
|
|
3
|
+
|
|
4
|
+
export interface CalculateCreditsParams {
|
|
5
|
+
metadata: InitializeCreditsMetadata;
|
|
6
|
+
existingData: UserCreditsDocumentRead;
|
|
7
|
+
creditLimit: number;
|
|
8
|
+
purchaseId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BuildCreditsDataParams {
|
|
12
|
+
existingData: UserCreditsDocumentRead;
|
|
13
|
+
newCredits: number;
|
|
14
|
+
creditLimit: number;
|
|
15
|
+
purchaseId: string;
|
|
16
|
+
metadata: InitializeCreditsMetadata;
|
|
17
|
+
purchaseHistory: any[];
|
|
18
|
+
platform: "ios" | "android";
|
|
19
|
+
}
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useCredits Hook
|
|
3
|
-
*
|
|
4
|
-
* Fetches user credits with TanStack Query best practices.
|
|
5
|
-
* Uses status-based state management for reliable loading detection.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
|
|
9
2
|
import { useCallback, useMemo, useEffect } from "react";
|
|
10
3
|
import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
|
|
@@ -50,7 +43,6 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
50
43
|
const userId = useAuthStore(selectUserId);
|
|
51
44
|
const isConfigured = isCreditsRepositoryConfigured();
|
|
52
45
|
|
|
53
|
-
// Only access config if configured to avoid throwing errors
|
|
54
46
|
const config = isConfigured ? getCreditsConfig() : null;
|
|
55
47
|
const queryEnabled = !!userId && isConfigured;
|
|
56
48
|
|
|
@@ -69,8 +61,6 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
69
61
|
return result.data || null;
|
|
70
62
|
},
|
|
71
63
|
enabled: queryEnabled,
|
|
72
|
-
staleTime: 30 * 1000, // 30 seconds - data considered fresh
|
|
73
|
-
gcTime: 5 * 60 * 1000, // 5 minutes - keep in cache after unmount
|
|
74
64
|
refetchOnMount: "always",
|
|
75
65
|
refetchOnWindowFocus: true,
|
|
76
66
|
refetchOnReconnect: true,
|
|
@@ -78,7 +68,6 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
78
68
|
|
|
79
69
|
const queryClient = useQueryClient();
|
|
80
70
|
|
|
81
|
-
// Observer Pattern: Listen for credit updates
|
|
82
71
|
useEffect(() => {
|
|
83
72
|
if (!userId) return;
|
|
84
73
|
|
|
@@ -126,3 +115,4 @@ export const useHasCredits = (): boolean => {
|
|
|
126
115
|
const { hasCredits } = useCredits();
|
|
127
116
|
return hasCredits;
|
|
128
117
|
};
|
|
118
|
+
|
|
@@ -1,151 +1,58 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Paywall Modal
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
1
|
import React, { useCallback, useEffect } from "react";
|
|
6
|
-
import { View, TouchableOpacity, Linking
|
|
2
|
+
import { View, TouchableOpacity, Linking } from "react-native";
|
|
7
3
|
import { BaseModal, useAppDesignTokens, AtomicText, AtomicIcon, AtomicSpinner, useSafeAreaInsets } from "@umituz/react-native-design-system";
|
|
8
4
|
import { ScreenLayout } from "../../../shared/presentation";
|
|
9
5
|
import { Image } from "expo-image";
|
|
10
|
-
import type { PurchasesPackage } from "react-native-purchases";
|
|
11
6
|
import { PlanCard } from "./PlanCard";
|
|
12
|
-
import type { SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from "../entities/types";
|
|
13
7
|
import { paywallModalStyles as styles } from "./PaywallModal.styles";
|
|
14
8
|
import { PaywallFeatures } from "./PaywallFeatures";
|
|
15
9
|
import { PaywallFooter } from "./PaywallFooter";
|
|
16
10
|
import { usePaywallActions } from "../hooks/usePaywallActions";
|
|
17
|
-
|
|
18
|
-
/** Trial eligibility info per product */
|
|
19
|
-
export interface TrialEligibilityInfo {
|
|
20
|
-
/** Whether eligible for trial */
|
|
21
|
-
eligible: boolean;
|
|
22
|
-
/** Trial duration in days */
|
|
23
|
-
durationDays?: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface PaywallModalProps {
|
|
27
|
-
visible: boolean;
|
|
28
|
-
onClose: () => void;
|
|
29
|
-
translations: PaywallTranslations;
|
|
30
|
-
packages?: PurchasesPackage[];
|
|
31
|
-
features?: SubscriptionFeature[];
|
|
32
|
-
isLoading?: boolean;
|
|
33
|
-
legalUrls?: PaywallLegalUrls;
|
|
34
|
-
bestValueIdentifier?: string;
|
|
35
|
-
/** Credit amounts per product identifier */
|
|
36
|
-
creditAmounts?: Record<string, number>;
|
|
37
|
-
/** Credits label text (e.g., "credits") */
|
|
38
|
-
creditsLabel?: string;
|
|
39
|
-
heroImage?: ImageSourcePropType;
|
|
40
|
-
onPurchase?: (pkg: PurchasesPackage) => Promise<void | boolean>;
|
|
41
|
-
onRestore?: () => Promise<void | boolean>;
|
|
42
|
-
/** Trial eligibility map per product ID */
|
|
43
|
-
trialEligibility?: Record<string, TrialEligibilityInfo>;
|
|
44
|
-
/** Trial subtitle text for PlanCard (e.g., "7 days free, then billed") */
|
|
45
|
-
trialSubtitleText?: string;
|
|
46
|
-
}
|
|
11
|
+
import { PaywallModalProps } from "./PaywallModal.types";
|
|
47
12
|
|
|
48
13
|
export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
49
14
|
const { visible, onClose, translations, packages = [], features = [], isLoading = false, legalUrls = {}, bestValueIdentifier, creditAmounts, creditsLabel, heroImage, onPurchase, onRestore, trialEligibility = {}, trialSubtitleText } = props;
|
|
50
15
|
const tokens = useAppDesignTokens();
|
|
51
16
|
const insets = useSafeAreaInsets();
|
|
52
|
-
|
|
53
|
-
const {
|
|
54
|
-
selectedPlanId,
|
|
55
|
-
setSelectedPlanId,
|
|
56
|
-
isProcessing,
|
|
57
|
-
handlePurchase,
|
|
58
|
-
handleRestore,
|
|
59
|
-
resetState,
|
|
60
|
-
} = usePaywallActions({ packages, onPurchase, onRestore });
|
|
61
|
-
|
|
62
|
-
// Reset selected plan when packages change
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
setSelectedPlanId(null);
|
|
65
|
-
}, [packages, setSelectedPlanId]);
|
|
17
|
+
const { selectedPlanId, setSelectedPlanId, isProcessing, handlePurchase, handleRestore, resetState } = usePaywallActions({ packages, onPurchase, onRestore });
|
|
66
18
|
|
|
67
|
-
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
if (!visible) {
|
|
70
|
-
resetState();
|
|
71
|
-
}
|
|
72
|
-
}, [visible, resetState]);
|
|
19
|
+
useEffect(() => { setSelectedPlanId(null); }, [packages, setSelectedPlanId]);
|
|
20
|
+
useEffect(() => { if (!visible) resetState(); }, [visible, resetState]);
|
|
73
21
|
|
|
74
22
|
const handleLegalUrl = useCallback(async (url: string | undefined) => {
|
|
75
23
|
if (!url) return;
|
|
76
|
-
try { if (await Linking.canOpenURL(url)) await Linking.openURL(url); } catch {
|
|
24
|
+
try { if (await Linking.canOpenURL(url)) await Linking.openURL(url); } catch { }
|
|
77
25
|
}, []);
|
|
78
26
|
|
|
79
27
|
return (
|
|
80
28
|
<BaseModal visible={visible} onClose={onClose} contentStyle={styles.modalContent}>
|
|
81
29
|
<View style={[styles.container, { backgroundColor: tokens.colors.surface }]}>
|
|
82
|
-
<TouchableOpacity
|
|
83
|
-
onPress={onClose}
|
|
84
|
-
style={[
|
|
85
|
-
styles.closeBtn,
|
|
86
|
-
{ backgroundColor: tokens.colors.surfaceSecondary, top: Math.max(insets.top, 12) }
|
|
87
|
-
]}
|
|
88
|
-
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
89
|
-
>
|
|
30
|
+
<TouchableOpacity onPress={onClose} style={[styles.closeBtn, { backgroundColor: tokens.colors.surfaceSecondary, top: Math.max(insets.top, 12) }]} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
|
90
31
|
<AtomicIcon name="close-outline" size="md" customColor={tokens.colors.textPrimary} />
|
|
91
32
|
</TouchableOpacity>
|
|
92
|
-
|
|
93
33
|
<ScreenLayout scrollable={true} edges={["bottom"]} backgroundColor="transparent" contentContainerStyle={styles.scroll}>
|
|
94
|
-
{heroImage &&
|
|
95
|
-
<View style={styles.heroContainer}>
|
|
96
|
-
<Image source={heroImage} style={styles.heroImage} contentFit="cover" transition={0} />
|
|
97
|
-
</View>
|
|
98
|
-
)}
|
|
99
|
-
|
|
34
|
+
{heroImage && <View style={styles.heroContainer}><Image source={heroImage} style={styles.heroImage} contentFit="cover" transition={0} /></View>}
|
|
100
35
|
<View style={styles.header}>
|
|
101
36
|
<AtomicText type="headlineMedium" style={[styles.title, { color: tokens.colors.textPrimary }]}>{translations.title}</AtomicText>
|
|
102
|
-
{
|
|
103
|
-
{translations.subtitle && (
|
|
104
|
-
<AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>
|
|
105
|
-
{translations.subtitle}
|
|
106
|
-
</AtomicText>
|
|
107
|
-
)}
|
|
37
|
+
{translations.subtitle && <AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>{translations.subtitle}</AtomicText>}
|
|
108
38
|
</View>
|
|
109
|
-
|
|
110
39
|
<PaywallFeatures features={features} />
|
|
111
|
-
|
|
112
40
|
{isLoading ? (
|
|
113
41
|
<View style={styles.loading}><AtomicSpinner size="lg" color="primary" text={translations.loadingText} /></View>
|
|
114
42
|
) : (
|
|
115
43
|
<View style={styles.plans}>
|
|
116
44
|
{packages.map((pkg) => {
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const hasFreeTrial = eligibility?.eligible ?? false;
|
|
120
|
-
|
|
45
|
+
const pid = pkg.product.identifier;
|
|
46
|
+
const hasTrial = trialEligibility[pid]?.eligible ?? false;
|
|
121
47
|
return (
|
|
122
|
-
<PlanCard
|
|
123
|
-
key={productId}
|
|
124
|
-
pkg={pkg}
|
|
125
|
-
isSelected={selectedPlanId === productId}
|
|
126
|
-
onSelect={() => setSelectedPlanId(productId)}
|
|
127
|
-
badge={productId === bestValueIdentifier ? translations.bestValueBadgeText : undefined}
|
|
128
|
-
creditAmount={creditAmounts?.[productId]}
|
|
129
|
-
creditsLabel={creditsLabel}
|
|
130
|
-
hasFreeTrial={hasFreeTrial}
|
|
131
|
-
trialSubtitleText={hasFreeTrial ? trialSubtitleText : undefined}
|
|
132
|
-
/>
|
|
48
|
+
<PlanCard key={pid} pkg={pkg} isSelected={selectedPlanId === pid} onSelect={() => setSelectedPlanId(pid)} badge={pid === bestValueIdentifier ? translations.bestValueBadgeText : undefined} creditAmount={creditAmounts?.[pid]} creditsLabel={creditsLabel} hasFreeTrial={hasTrial} trialSubtitleText={hasTrial ? trialSubtitleText : undefined} />
|
|
133
49
|
);
|
|
134
50
|
})}
|
|
135
51
|
</View>
|
|
136
52
|
)}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
onPress={handlePurchase}
|
|
140
|
-
disabled={!selectedPlanId || isProcessing}
|
|
141
|
-
style={[styles.cta, { backgroundColor: tokens.colors.primary }, (!selectedPlanId || isProcessing) && styles.ctaDisabled]}
|
|
142
|
-
activeOpacity={0.8}
|
|
143
|
-
>
|
|
144
|
-
<AtomicText type="titleLarge" style={[styles.ctaText, { color: tokens.colors.onPrimary }]}>
|
|
145
|
-
{isProcessing ? translations.processingText : translations.purchaseButtonText}
|
|
146
|
-
</AtomicText>
|
|
53
|
+
<TouchableOpacity onPress={handlePurchase} disabled={!selectedPlanId || isProcessing} style={[styles.cta, { backgroundColor: tokens.colors.primary }, (!selectedPlanId || isProcessing) && styles.ctaDisabled]} activeOpacity={0.8}>
|
|
54
|
+
<AtomicText type="titleLarge" style={[styles.ctaText, { color: tokens.colors.onPrimary }]}>{isProcessing ? translations.processingText : translations.purchaseButtonText}</AtomicText>
|
|
147
55
|
</TouchableOpacity>
|
|
148
|
-
|
|
149
56
|
<PaywallFooter translations={translations} legalUrls={legalUrls} isProcessing={isProcessing} onRestore={onRestore ? handleRestore : undefined} onLegalClick={handleLegalUrl} />
|
|
150
57
|
</ScreenLayout>
|
|
151
58
|
</View>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ImageSourcePropType } from "react-native";
|
|
2
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
|
+
import type { SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from "../entities/types";
|
|
4
|
+
|
|
5
|
+
export interface TrialEligibilityInfo {
|
|
6
|
+
eligible: boolean;
|
|
7
|
+
durationDays?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PaywallModalProps {
|
|
11
|
+
visible: boolean;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
translations: PaywallTranslations;
|
|
14
|
+
packages?: PurchasesPackage[];
|
|
15
|
+
features?: SubscriptionFeature[];
|
|
16
|
+
isLoading?: boolean;
|
|
17
|
+
legalUrls?: PaywallLegalUrls;
|
|
18
|
+
bestValueIdentifier?: string;
|
|
19
|
+
creditAmounts?: Record<string, number>;
|
|
20
|
+
creditsLabel?: string;
|
|
21
|
+
heroImage?: ImageSourcePropType;
|
|
22
|
+
onPurchase?: (pkg: PurchasesPackage) => Promise<void | boolean>;
|
|
23
|
+
onRestore?: () => Promise<void | boolean>;
|
|
24
|
+
trialEligibility?: Record<string, TrialEligibilityInfo>;
|
|
25
|
+
trialSubtitleText?: string;
|
|
26
|
+
}
|