@umituz/react-native-subscription 2.12.1 → 2.12.3

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.
Files changed (45) hide show
  1. package/package.json +3 -3
  2. package/src/domains/index.ts +5 -0
  3. package/src/domains/paywall/components/CreditCard.tsx +117 -0
  4. package/src/domains/paywall/components/FeatureItem.tsx +50 -0
  5. package/src/domains/paywall/components/FeatureList.tsx +34 -0
  6. package/src/domains/paywall/components/PaywallFooter.tsx +98 -0
  7. package/src/domains/paywall/components/PaywallHeader.tsx +115 -0
  8. package/src/domains/paywall/components/PaywallModal.tsx +187 -0
  9. package/src/domains/paywall/components/PaywallTabBar.tsx +102 -0
  10. package/src/domains/paywall/components/PlanCard.tsx +124 -0
  11. package/src/domains/paywall/components/index.ts +14 -0
  12. package/src/domains/paywall/entities/index.ts +5 -0
  13. package/src/domains/paywall/entities/types.ts +48 -0
  14. package/src/domains/paywall/hooks/index.ts +6 -0
  15. package/src/{presentation → domains/paywall}/hooks/usePaywall.ts +1 -1
  16. package/src/domains/paywall/index.ts +13 -0
  17. package/src/index.ts +17 -21
  18. package/src/domain/entities/paywall/CreditsPackage.ts +0 -16
  19. package/src/domain/entities/paywall/PaywallTab.ts +0 -11
  20. package/src/domain/entities/paywall/SubscriptionPlan.ts +0 -27
  21. package/src/presentation/components/paywall/BestValueBadge.tsx +0 -59
  22. package/src/presentation/components/paywall/CreditsPackageCard.tsx +0 -129
  23. package/src/presentation/components/paywall/CreditsTabContent.tsx +0 -123
  24. package/src/presentation/components/paywall/PaywallFeatureItem.tsx +0 -65
  25. package/src/presentation/components/paywall/PaywallFeaturesList.tsx +0 -47
  26. package/src/presentation/components/paywall/PaywallHeader.tsx +0 -82
  27. package/src/presentation/components/paywall/PaywallLegalFooter.tsx +0 -145
  28. package/src/presentation/components/paywall/PaywallLegalFooterStyles.ts +0 -53
  29. package/src/presentation/components/paywall/PaywallLegalFooterTypes.ts +0 -19
  30. package/src/presentation/components/paywall/PaywallModal.tsx +0 -162
  31. package/src/presentation/components/paywall/PaywallTabBar.tsx +0 -96
  32. package/src/presentation/components/paywall/SubscriptionFooter.tsx +0 -116
  33. package/src/presentation/components/paywall/SubscriptionModal.tsx +0 -168
  34. package/src/presentation/components/paywall/SubscriptionModalHeader.tsx +0 -78
  35. package/src/presentation/components/paywall/SubscriptionPackageList.tsx +0 -171
  36. package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +0 -213
  37. package/src/presentation/components/paywall/SubscriptionPlanCardStyles.ts +0 -61
  38. package/src/presentation/components/paywall/SubscriptionPlanCardTypes.ts +0 -15
  39. package/src/presentation/components/paywall/SubscriptionTabContent.tsx +0 -139
  40. package/src/presentation/components/paywall/accordion/AccordionPlanCard.tsx +0 -100
  41. package/src/presentation/components/paywall/accordion/AccordionPlanCardTypes.ts +0 -39
  42. package/src/presentation/components/paywall/accordion/PlanCardDetails.tsx +0 -107
  43. package/src/presentation/components/paywall/accordion/PlanCardHeader.tsx +0 -165
  44. package/src/presentation/components/paywall/accordion/index.ts +0 -12
  45. /package/src/{presentation → domains/paywall}/hooks/useSubscriptionModal.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.12.1",
3
+ "version": "2.12.3",
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",
@@ -47,7 +47,7 @@
47
47
  "devDependencies": {
48
48
  "@tanstack/react-query": "^5.0.0",
49
49
  "@types/react": "~19.1.10",
50
- "@umituz/react-native-design-system": "^2.2.0",
50
+ "@umituz/react-native-design-system": "^2.3.1",
51
51
  "@umituz/react-native-firebase": "*",
52
52
  "@umituz/react-native-legal": "*",
53
53
  "@umituz/react-native-localization": "*",
@@ -70,4 +70,4 @@
70
70
  "LICENSE"
71
71
  ],
72
72
  "dependencies": {}
73
- }
73
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Domains Index
3
+ */
4
+
5
+ export * from "./paywall";
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Credit Card
3
+ * Credit package selection card
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, StyleSheet } from "react-native";
8
+ import { AtomicText, AtomicIcon, AtomicBadge, useAppDesignTokens } from "@umituz/react-native-design-system";
9
+ import type { CreditsPackage } from "../entities";
10
+
11
+ interface CreditCardProps {
12
+ pkg: CreditsPackage;
13
+ isSelected: boolean;
14
+ onSelect: () => void;
15
+ }
16
+
17
+ export const CreditCard: React.FC<CreditCardProps> = React.memo(({ pkg, isSelected, onSelect }) => {
18
+ const tokens = useAppDesignTokens();
19
+ const totalCredits = pkg.credits + (pkg.bonus ?? 0);
20
+
21
+ return (
22
+ <TouchableOpacity onPress={onSelect} activeOpacity={0.7} style={styles.touchable}>
23
+ <View
24
+ style={[
25
+ styles.container,
26
+ {
27
+ backgroundColor: tokens.colors.surface,
28
+ borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
29
+ borderWidth: isSelected ? 2 : 1,
30
+ },
31
+ ]}
32
+ >
33
+ {pkg.badge && (
34
+ <View style={styles.badgeContainer}>
35
+ <AtomicBadge text={pkg.badge} variant="warning" size="sm" />
36
+ </View>
37
+ )}
38
+
39
+ <View style={styles.content}>
40
+ <View style={styles.leftSection}>
41
+ <AtomicIcon name="flash" size="md" color={isSelected ? "primary" : "secondary"} />
42
+ <AtomicText
43
+ type="headlineSmall"
44
+ style={[styles.credits, { color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary }]}
45
+ >
46
+ {totalCredits.toLocaleString()}
47
+ </AtomicText>
48
+ </View>
49
+
50
+ <View style={styles.rightSection}>
51
+ <AtomicText
52
+ type="titleLarge"
53
+ style={[styles.price, { color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary }]}
54
+ >
55
+ {pkg.currency}{pkg.price.toFixed(2)}
56
+ </AtomicText>
57
+ {isSelected && <AtomicIcon name="checkmark-circle" size="md" color="primary" />}
58
+ </View>
59
+ </View>
60
+
61
+ {(pkg.bonus ?? 0) > 0 && (
62
+ <View style={styles.bonusRow}>
63
+ <AtomicIcon name="gift-outline" size="sm" color="success" />
64
+ <AtomicText type="bodySmall" style={{ color: tokens.colors.success, marginLeft: 4 }}>
65
+ +{pkg.bonus}
66
+ </AtomicText>
67
+ </View>
68
+ )}
69
+ </View>
70
+ </TouchableOpacity>
71
+ );
72
+ });
73
+
74
+ CreditCard.displayName = "CreditCard";
75
+
76
+ const styles = StyleSheet.create({
77
+ touchable: {
78
+ marginBottom: 10,
79
+ marginHorizontal: 24,
80
+ },
81
+ container: {
82
+ borderRadius: 16,
83
+ padding: 16,
84
+ position: "relative",
85
+ },
86
+ badgeContainer: {
87
+ position: "absolute",
88
+ top: -10,
89
+ right: 16,
90
+ },
91
+ content: {
92
+ flexDirection: "row",
93
+ justifyContent: "space-between",
94
+ alignItems: "center",
95
+ },
96
+ leftSection: {
97
+ flexDirection: "row",
98
+ alignItems: "center",
99
+ },
100
+ credits: {
101
+ fontWeight: "700",
102
+ marginLeft: 8,
103
+ },
104
+ rightSection: {
105
+ flexDirection: "row",
106
+ alignItems: "center",
107
+ },
108
+ price: {
109
+ fontWeight: "700",
110
+ marginRight: 8,
111
+ },
112
+ bonusRow: {
113
+ flexDirection: "row",
114
+ alignItems: "center",
115
+ marginTop: 8,
116
+ },
117
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Feature Item
3
+ * Single feature row with icon
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { AtomicText, AtomicIcon, useAppDesignTokens } from "@umituz/react-native-design-system";
9
+
10
+ interface FeatureItemProps {
11
+ icon: string;
12
+ text: string;
13
+ }
14
+
15
+ export const FeatureItem: React.FC<FeatureItemProps> = React.memo(({ icon, text }) => {
16
+ const tokens = useAppDesignTokens();
17
+
18
+ return (
19
+ <View style={styles.container}>
20
+ <View style={[styles.iconContainer, { backgroundColor: tokens.colors.primaryLight }]}>
21
+ <AtomicIcon
22
+ name={icon || "checkmark-circle"}
23
+ customSize={16}
24
+ customColor={tokens.colors.primary}
25
+ />
26
+ </View>
27
+ <AtomicText type="bodyMedium" style={{ color: tokens.colors.textPrimary, flex: 1 }}>
28
+ {text}
29
+ </AtomicText>
30
+ </View>
31
+ );
32
+ });
33
+
34
+ FeatureItem.displayName = "FeatureItem";
35
+
36
+ const styles = StyleSheet.create({
37
+ container: {
38
+ flexDirection: "row",
39
+ alignItems: "center",
40
+ marginBottom: 12,
41
+ },
42
+ iconContainer: {
43
+ width: 28,
44
+ height: 28,
45
+ borderRadius: 14,
46
+ justifyContent: "center",
47
+ alignItems: "center",
48
+ marginRight: 12,
49
+ },
50
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Feature List
3
+ * List of premium features
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { FeatureItem } from "./FeatureItem";
9
+ import type { SubscriptionFeature } from "../entities";
10
+
11
+ interface FeatureListProps {
12
+ features: SubscriptionFeature[];
13
+ }
14
+
15
+ export const FeatureList: React.FC<FeatureListProps> = React.memo(({ features }) => {
16
+ if (features.length === 0) return null;
17
+
18
+ return (
19
+ <View style={styles.container}>
20
+ {features.map((feature, index) => (
21
+ <FeatureItem key={`${feature.icon}-${index}`} icon={feature.icon} text={feature.text} />
22
+ ))}
23
+ </View>
24
+ );
25
+ });
26
+
27
+ FeatureList.displayName = "FeatureList";
28
+
29
+ const styles = StyleSheet.create({
30
+ container: {
31
+ paddingHorizontal: 24,
32
+ marginBottom: 20,
33
+ },
34
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Paywall Footer
3
+ * Action button and legal links
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, StyleSheet, Linking } from "react-native";
8
+ import { AtomicText, AtomicButton, useAppDesignTokens } from "@umituz/react-native-design-system";
9
+
10
+ interface PaywallFooterProps {
11
+ isProcessing: boolean;
12
+ isDisabled: boolean;
13
+ purchaseButtonText: string;
14
+ processingText: string;
15
+ restoreButtonText: string;
16
+ privacyText?: string;
17
+ termsText?: string;
18
+ privacyUrl?: string;
19
+ termsUrl?: string;
20
+ onPurchase: () => void;
21
+ onRestore: () => void;
22
+ }
23
+
24
+ export const PaywallFooter: React.FC<PaywallFooterProps> = React.memo(
25
+ ({
26
+ isProcessing,
27
+ isDisabled,
28
+ purchaseButtonText,
29
+ processingText,
30
+ restoreButtonText,
31
+ privacyText,
32
+ termsText,
33
+ privacyUrl,
34
+ termsUrl,
35
+ onPurchase,
36
+ onRestore,
37
+ }) => {
38
+ const tokens = useAppDesignTokens();
39
+
40
+ const handleOpenUrl = (url?: string) => {
41
+ if (url) Linking.openURL(url);
42
+ };
43
+
44
+ return (
45
+ <View style={styles.container}>
46
+ <AtomicButton
47
+ title={isProcessing ? processingText : purchaseButtonText}
48
+ onPress={onPurchase}
49
+ disabled={isDisabled || isProcessing}
50
+ variant="primary"
51
+ size="lg"
52
+ style={styles.purchaseButton}
53
+ />
54
+
55
+ <View style={styles.linksRow}>
56
+ {termsText && termsUrl && (
57
+ <TouchableOpacity onPress={() => handleOpenUrl(termsUrl)}>
58
+ <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
59
+ {termsText}
60
+ </AtomicText>
61
+ </TouchableOpacity>
62
+ )}
63
+
64
+ <TouchableOpacity onPress={onRestore}>
65
+ <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
66
+ {restoreButtonText}
67
+ </AtomicText>
68
+ </TouchableOpacity>
69
+
70
+ {privacyText && privacyUrl && (
71
+ <TouchableOpacity onPress={() => handleOpenUrl(privacyUrl)}>
72
+ <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
73
+ {privacyText}
74
+ </AtomicText>
75
+ </TouchableOpacity>
76
+ )}
77
+ </View>
78
+ </View>
79
+ );
80
+ }
81
+ );
82
+
83
+ PaywallFooter.displayName = "PaywallFooter";
84
+
85
+ const styles = StyleSheet.create({
86
+ container: {
87
+ paddingHorizontal: 24,
88
+ paddingBottom: 32,
89
+ },
90
+ purchaseButton: {
91
+ marginBottom: 16,
92
+ },
93
+ linksRow: {
94
+ flexDirection: "row",
95
+ justifyContent: "space-between",
96
+ paddingHorizontal: 8,
97
+ },
98
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Paywall Header
3
+ * Header with gradient, close button, title and subtitle
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet, TouchableOpacity } from "react-native";
8
+ import { LinearGradient } from "expo-linear-gradient";
9
+ import {
10
+ AtomicText,
11
+ AtomicIcon,
12
+ useDesignSystemTheme,
13
+ useAppDesignTokens,
14
+ } from "@umituz/react-native-design-system";
15
+
16
+ interface PaywallHeaderProps {
17
+ title: string;
18
+ subtitle?: string;
19
+ onClose: () => void;
20
+ }
21
+
22
+ export const PaywallHeader: React.FC<PaywallHeaderProps> = React.memo(
23
+ ({ title, subtitle, onClose }) => {
24
+ const tokens = useAppDesignTokens();
25
+ const { themeMode } = useDesignSystemTheme();
26
+ const isDark = themeMode === "dark";
27
+
28
+ const gradientColors: readonly [string, string] = isDark
29
+ ? [tokens.colors.surface, tokens.colors.surfaceSecondary]
30
+ : [tokens.colors.primary, tokens.colors.primaryDark];
31
+
32
+ return (
33
+ <LinearGradient
34
+ colors={gradientColors}
35
+ start={{ x: 0, y: 0 }}
36
+ end={{ x: 1, y: 1 }}
37
+ style={styles.container}
38
+ >
39
+ <TouchableOpacity
40
+ onPress={onClose}
41
+ style={[styles.closeButton, { backgroundColor: tokens.colors.onPrimary }]}
42
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
43
+ >
44
+ <AtomicIcon
45
+ name="close-outline"
46
+ size="md"
47
+ customColor={isDark ? tokens.colors.textPrimary : tokens.colors.primary}
48
+ />
49
+ </TouchableOpacity>
50
+
51
+ <View style={styles.content}>
52
+ <AtomicText
53
+ type="headlineLarge"
54
+ style={[styles.title, { color: tokens.colors.onPrimary }]}
55
+ >
56
+ {title}
57
+ </AtomicText>
58
+ {subtitle && (
59
+ <AtomicText
60
+ type="bodyLarge"
61
+ style={[styles.subtitle, { color: tokens.colors.onPrimary }]}
62
+ >
63
+ {subtitle}
64
+ </AtomicText>
65
+ )}
66
+ </View>
67
+
68
+ <View style={[styles.wave, { backgroundColor: tokens.colors.background }]} />
69
+ </LinearGradient>
70
+ );
71
+ }
72
+ );
73
+
74
+ PaywallHeader.displayName = "PaywallHeader";
75
+
76
+ const styles = StyleSheet.create({
77
+ container: {
78
+ paddingTop: 56,
79
+ paddingBottom: 36,
80
+ paddingHorizontal: 24,
81
+ position: "relative",
82
+ },
83
+ closeButton: {
84
+ position: "absolute",
85
+ top: 48,
86
+ right: 20,
87
+ width: 36,
88
+ height: 36,
89
+ borderRadius: 18,
90
+ justifyContent: "center",
91
+ alignItems: "center",
92
+ zIndex: 10,
93
+ },
94
+ content: {
95
+ alignItems: "center",
96
+ },
97
+ title: {
98
+ fontWeight: "700",
99
+ textAlign: "center",
100
+ marginBottom: 8,
101
+ },
102
+ subtitle: {
103
+ textAlign: "center",
104
+ opacity: 0.9,
105
+ },
106
+ wave: {
107
+ position: "absolute",
108
+ bottom: -1,
109
+ left: 0,
110
+ right: 0,
111
+ height: 24,
112
+ borderTopLeftRadius: 24,
113
+ borderTopRightRadius: 24,
114
+ },
115
+ });
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Paywall Modal
3
+ * Mode-based paywall: subscription, credits, or hybrid
4
+ */
5
+
6
+ import React, { useState, useCallback } from "react";
7
+ import { View, ScrollView, StyleSheet, ActivityIndicator } from "react-native";
8
+ import { BaseModal, useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
9
+ import type { PurchasesPackage } from "react-native-purchases";
10
+ import { PaywallHeader } from "./PaywallHeader";
11
+ import { PaywallTabBar } from "./PaywallTabBar";
12
+ import { PaywallFooter } from "./PaywallFooter";
13
+ import { FeatureList } from "./FeatureList";
14
+ import { PlanCard } from "./PlanCard";
15
+ import { CreditCard } from "./CreditCard";
16
+ import type {
17
+ PaywallMode,
18
+ PaywallTabType,
19
+ CreditsPackage,
20
+ SubscriptionFeature,
21
+ PaywallTranslations,
22
+ PaywallLegalUrls,
23
+ } from "../entities";
24
+
25
+ export interface PaywallModalProps {
26
+ visible: boolean;
27
+ onClose: () => void;
28
+ mode: PaywallMode;
29
+ translations: PaywallTranslations;
30
+ subscriptionPackages?: PurchasesPackage[];
31
+ creditsPackages?: CreditsPackage[];
32
+ features?: SubscriptionFeature[];
33
+ isLoading?: boolean;
34
+ legalUrls?: PaywallLegalUrls;
35
+ bestValueIdentifier?: string;
36
+ creditAmounts?: Record<string, number>;
37
+ creditsLabel?: string;
38
+ onSubscriptionPurchase?: (pkg: PurchasesPackage) => Promise<void>;
39
+ onCreditsPurchase?: (packageId: string) => Promise<void>;
40
+ onRestore?: () => Promise<void>;
41
+ }
42
+
43
+ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
44
+ const {
45
+ visible,
46
+ onClose,
47
+ mode,
48
+ translations,
49
+ subscriptionPackages = [],
50
+ creditsPackages = [],
51
+ features = [],
52
+ isLoading = false,
53
+ legalUrls = {},
54
+ bestValueIdentifier,
55
+ creditAmounts,
56
+ creditsLabel,
57
+ onSubscriptionPurchase,
58
+ onCreditsPurchase,
59
+ onRestore,
60
+ } = props;
61
+
62
+ const tokens = useAppDesignTokens();
63
+ const initialTab: PaywallTabType = mode === "credits" ? "credits" : "subscription";
64
+ const [activeTab, setActiveTab] = useState<PaywallTabType>(initialTab);
65
+ const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
66
+ const [selectedCreditId, setSelectedCreditId] = useState<string | null>(null);
67
+ const [isProcessing, setIsProcessing] = useState(false);
68
+
69
+ const showTabs = mode === "hybrid";
70
+ const showCredits = mode === "credits" || (mode === "hybrid" && activeTab === "credits");
71
+ const showSubscription = mode === "subscription" || (mode === "hybrid" && activeTab === "subscription");
72
+
73
+ const handlePurchase = useCallback(async () => {
74
+ setIsProcessing(true);
75
+ try {
76
+ if (showSubscription && selectedPlanId && onSubscriptionPurchase) {
77
+ const pkg = subscriptionPackages.find((p) => p.product.identifier === selectedPlanId);
78
+ if (pkg) await onSubscriptionPurchase(pkg);
79
+ } else if (showCredits && selectedCreditId && onCreditsPurchase) {
80
+ await onCreditsPurchase(selectedCreditId);
81
+ }
82
+ } finally {
83
+ setIsProcessing(false);
84
+ }
85
+ }, [showSubscription, showCredits, selectedPlanId, selectedCreditId, subscriptionPackages, onSubscriptionPurchase, onCreditsPurchase]);
86
+
87
+ const handleRestore = useCallback(async () => {
88
+ if (onRestore) {
89
+ setIsProcessing(true);
90
+ try {
91
+ await onRestore();
92
+ } finally {
93
+ setIsProcessing(false);
94
+ }
95
+ }
96
+ }, [onRestore]);
97
+
98
+ const isPurchaseDisabled = showSubscription ? !selectedPlanId : !selectedCreditId;
99
+
100
+ return (
101
+ <BaseModal visible={visible} onClose={onClose}>
102
+ <View style={styles.container}>
103
+ <PaywallHeader title={translations.title} subtitle={translations.subtitle} onClose={onClose} />
104
+
105
+ {showTabs && (
106
+ <PaywallTabBar
107
+ activeTab={activeTab}
108
+ onTabChange={setActiveTab}
109
+ creditsLabel={translations.creditsTabLabel ?? "Credits"}
110
+ subscriptionLabel={translations.subscriptionTabLabel ?? "Subscription"}
111
+ />
112
+ )}
113
+
114
+ <ScrollView style={styles.scroll} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
115
+ <FeatureList features={features} />
116
+
117
+ {isLoading ? (
118
+ <View style={styles.loadingContainer}>
119
+ <ActivityIndicator color={tokens.colors.primary} />
120
+ <AtomicText type="bodyMedium" style={{ color: tokens.colors.textSecondary, marginTop: 12 }}>
121
+ {translations.loadingText}
122
+ </AtomicText>
123
+ </View>
124
+ ) : (
125
+ <>
126
+ {showSubscription &&
127
+ subscriptionPackages.map((pkg) => (
128
+ <PlanCard
129
+ key={pkg.product.identifier}
130
+ pkg={pkg}
131
+ isSelected={selectedPlanId === pkg.product.identifier}
132
+ onSelect={() => setSelectedPlanId(pkg.product.identifier)}
133
+ badge={pkg.product.identifier === bestValueIdentifier ? "Best Value" : undefined}
134
+ creditAmount={creditAmounts?.[pkg.product.identifier]}
135
+ creditsLabel={creditsLabel}
136
+ />
137
+ ))}
138
+
139
+ {showCredits &&
140
+ creditsPackages.map((pkg) => (
141
+ <CreditCard
142
+ key={pkg.id}
143
+ pkg={pkg}
144
+ isSelected={selectedCreditId === pkg.id}
145
+ onSelect={() => setSelectedCreditId(pkg.id)}
146
+ />
147
+ ))}
148
+ </>
149
+ )}
150
+ </ScrollView>
151
+
152
+ <PaywallFooter
153
+ isProcessing={isProcessing}
154
+ isDisabled={isPurchaseDisabled}
155
+ purchaseButtonText={showSubscription ? (translations.subscribeButtonText ?? translations.purchaseButtonText) : translations.purchaseButtonText}
156
+ processingText={translations.processingText}
157
+ restoreButtonText={translations.restoreButtonText}
158
+ privacyText={translations.privacyText}
159
+ termsText={translations.termsOfServiceText}
160
+ privacyUrl={legalUrls.privacyUrl}
161
+ termsUrl={legalUrls.termsUrl}
162
+ onPurchase={handlePurchase}
163
+ onRestore={handleRestore}
164
+ />
165
+ </View>
166
+ </BaseModal>
167
+ );
168
+ });
169
+
170
+ PaywallModal.displayName = "PaywallModal";
171
+
172
+ const styles = StyleSheet.create({
173
+ container: {
174
+ flex: 1,
175
+ width: "100%",
176
+ },
177
+ scroll: {
178
+ flex: 1,
179
+ },
180
+ scrollContent: {
181
+ paddingBottom: 20,
182
+ },
183
+ loadingContainer: {
184
+ alignItems: "center",
185
+ paddingVertical: 40,
186
+ },
187
+ });