@umituz/react-native-subscription 2.12.2 → 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 (46) hide show
  1. package/package.json +1 -1
  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/{presentation/components/paywall/PaywallHeroHeader.tsx → domains/paywall/components/PaywallHeader.tsx} +15 -44
  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 +15 -22
  18. package/src/domain/entities/paywall/CreditsPackage.ts +0 -16
  19. package/src/domain/entities/paywall/PaywallMode.ts +0 -6
  20. package/src/domain/entities/paywall/PaywallTab.ts +0 -11
  21. package/src/domain/entities/paywall/SubscriptionPlan.ts +0 -27
  22. package/src/presentation/components/paywall/BestValueBadge.tsx +0 -59
  23. package/src/presentation/components/paywall/CreditsPackageCard.tsx +0 -161
  24. package/src/presentation/components/paywall/CreditsTabContent.tsx +0 -123
  25. package/src/presentation/components/paywall/PaywallFeatureItem.tsx +0 -79
  26. package/src/presentation/components/paywall/PaywallFeaturesList.tsx +0 -47
  27. package/src/presentation/components/paywall/PaywallHeader.tsx +0 -82
  28. package/src/presentation/components/paywall/PaywallLegalFooter.tsx +0 -145
  29. package/src/presentation/components/paywall/PaywallLegalFooterStyles.ts +0 -53
  30. package/src/presentation/components/paywall/PaywallLegalFooterTypes.ts +0 -19
  31. package/src/presentation/components/paywall/PaywallModal.tsx +0 -162
  32. package/src/presentation/components/paywall/PaywallTabBar.tsx +0 -120
  33. package/src/presentation/components/paywall/SubscriptionFooter.tsx +0 -116
  34. package/src/presentation/components/paywall/SubscriptionModal.tsx +0 -168
  35. package/src/presentation/components/paywall/SubscriptionModalHeader.tsx +0 -78
  36. package/src/presentation/components/paywall/SubscriptionPackageList.tsx +0 -171
  37. package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +0 -213
  38. package/src/presentation/components/paywall/SubscriptionPlanCardStyles.ts +0 -61
  39. package/src/presentation/components/paywall/SubscriptionPlanCardTypes.ts +0 -15
  40. package/src/presentation/components/paywall/SubscriptionTabContent.tsx +0 -139
  41. package/src/presentation/components/paywall/accordion/AccordionPlanCard.tsx +0 -98
  42. package/src/presentation/components/paywall/accordion/AccordionPlanCardTypes.ts +0 -39
  43. package/src/presentation/components/paywall/accordion/PlanCardDetails.tsx +0 -107
  44. package/src/presentation/components/paywall/accordion/PlanCardHeader.tsx +0 -155
  45. package/src/presentation/components/paywall/accordion/index.ts +0 -12
  46. /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.2",
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",
@@ -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
+ });
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Paywall Hero Header Component
3
- * Header with gradient background - theme aware
2
+ * Paywall Header
3
+ * Header with gradient, close button, title and subtitle
4
4
  */
5
5
 
6
6
  import React from "react";
@@ -13,21 +13,21 @@ import {
13
13
  useAppDesignTokens,
14
14
  } from "@umituz/react-native-design-system";
15
15
 
16
- interface PaywallHeroHeaderProps {
16
+ interface PaywallHeaderProps {
17
17
  title: string;
18
18
  subtitle?: string;
19
19
  onClose: () => void;
20
20
  }
21
21
 
22
- export const PaywallHeroHeader: React.FC<PaywallHeroHeaderProps> = React.memo(
22
+ export const PaywallHeader: React.FC<PaywallHeaderProps> = React.memo(
23
23
  ({ title, subtitle, onClose }) => {
24
24
  const tokens = useAppDesignTokens();
25
25
  const { themeMode } = useDesignSystemTheme();
26
26
  const isDark = themeMode === "dark";
27
27
 
28
- const gradientColors: readonly [string, string, string] = isDark
29
- ? [tokens.colors.background, tokens.colors.surfaceSecondary, tokens.colors.surface]
30
- : [tokens.colors.primary, tokens.colors.primaryDark, tokens.colors.primary];
28
+ const gradientColors: readonly [string, string] = isDark
29
+ ? [tokens.colors.surface, tokens.colors.surfaceSecondary]
30
+ : [tokens.colors.primary, tokens.colors.primaryDark];
31
31
 
32
32
  return (
33
33
  <LinearGradient
@@ -36,19 +36,9 @@ export const PaywallHeroHeader: React.FC<PaywallHeroHeaderProps> = React.memo(
36
36
  end={{ x: 1, y: 1 }}
37
37
  style={styles.container}
38
38
  >
39
- <View style={[styles.decorativeCircle, styles.circle1]} />
40
- <View style={[styles.decorativeCircle, styles.circle2]} />
41
-
42
39
  <TouchableOpacity
43
40
  onPress={onClose}
44
- style={[
45
- styles.closeButton,
46
- {
47
- backgroundColor: isDark
48
- ? tokens.colors.surfaceSecondary
49
- : tokens.colors.onPrimary,
50
- },
51
- ]}
41
+ style={[styles.closeButton, { backgroundColor: tokens.colors.onPrimary }]}
52
42
  hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
53
43
  >
54
44
  <AtomicIcon
@@ -81,36 +71,18 @@ export const PaywallHeroHeader: React.FC<PaywallHeroHeaderProps> = React.memo(
81
71
  }
82
72
  );
83
73
 
84
- PaywallHeroHeader.displayName = "PaywallHeroHeader";
74
+ PaywallHeader.displayName = "PaywallHeader";
85
75
 
86
76
  const styles = StyleSheet.create({
87
77
  container: {
88
- paddingTop: 60,
89
- paddingBottom: 40,
78
+ paddingTop: 56,
79
+ paddingBottom: 36,
90
80
  paddingHorizontal: 24,
91
81
  position: "relative",
92
- overflow: "hidden",
93
- },
94
- decorativeCircle: {
95
- position: "absolute",
96
- borderRadius: 9999,
97
- backgroundColor: "rgba(255, 255, 255, 0.1)",
98
- },
99
- circle1: {
100
- width: 200,
101
- height: 200,
102
- top: -100,
103
- right: -50,
104
- },
105
- circle2: {
106
- width: 150,
107
- height: 150,
108
- bottom: -75,
109
- left: -40,
110
82
  },
111
83
  closeButton: {
112
84
  position: "absolute",
113
- top: 50,
85
+ top: 48,
114
86
  right: 20,
115
87
  width: 36,
116
88
  height: 36,
@@ -121,7 +93,6 @@ const styles = StyleSheet.create({
121
93
  },
122
94
  content: {
123
95
  alignItems: "center",
124
- zIndex: 1,
125
96
  },
126
97
  title: {
127
98
  fontWeight: "700",
@@ -137,8 +108,8 @@ const styles = StyleSheet.create({
137
108
  bottom: -1,
138
109
  left: 0,
139
110
  right: 0,
140
- height: 30,
141
- borderTopLeftRadius: 30,
142
- borderTopRightRadius: 30,
111
+ height: 24,
112
+ borderTopLeftRadius: 24,
113
+ borderTopRightRadius: 24,
143
114
  },
144
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
+ });