@umituz/react-native-subscription 2.2.7 → 2.2.10

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.2.7",
3
+ "version": "2.2.10",
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",
@@ -7,7 +7,7 @@ import React from "react";
7
7
  import { View, StyleSheet, TouchableOpacity } from "react-native";
8
8
  import { AtomicText } from "@umituz/react-native-design-system-atoms";
9
9
  import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
10
- import type { CreditsPackage } from "../../domain/entities/CreditsPackage";
10
+ import type { CreditsPackage } from "../../../domain/entities/paywall/CreditsPackage";
11
11
 
12
12
  interface CreditsPackageCardProps {
13
13
  package: CreditsPackage;
@@ -10,7 +10,7 @@ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
10
10
  import { useLocalization } from "@umituz/react-native-localization";
11
11
  import { CreditsPackageCard } from "./CreditsPackageCard";
12
12
  import { PaywallLegalFooter } from "./PaywallLegalFooter";
13
- import type { CreditsPackage } from "../../domain/entities/CreditsPackage";
13
+ import type { CreditsPackage } from "../../../domain/entities/paywall/CreditsPackage";
14
14
 
15
15
  interface CreditsTabContentProps {
16
16
  packages: CreditsPackage[];
@@ -42,7 +42,7 @@ export const CreditsTabContent: React.FC<CreditsTabContentProps> = React.memo(
42
42
  const { t } = useLocalization();
43
43
 
44
44
  const needsCredits = requiredCredits && requiredCredits > currentCredits;
45
-
45
+
46
46
  const displayPurchaseButtonText = purchaseButtonText ||
47
47
  t("paywall.purchase", { defaultValue: "Purchase" });
48
48
  const displayProcessingText = processingText ||
@@ -9,13 +9,13 @@ import { SafeAreaView } from "react-native-safe-area-context";
9
9
  import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
10
10
  import { useLocalization } from "@umituz/react-native-localization";
11
11
  import type { PurchasesPackage } from "react-native-purchases";
12
- import { usePaywall } from "../hooks/usePaywall";
12
+ import { usePaywall } from "../../hooks/usePaywall";
13
13
  import { PaywallHeader } from "./PaywallHeader";
14
14
  import { PaywallTabBar } from "./PaywallTabBar";
15
15
  import { CreditsTabContent } from "./CreditsTabContent";
16
16
  import { SubscriptionTabContent } from "./SubscriptionTabContent";
17
- import type { PaywallTabType } from "../../domain/entities/PaywallTab";
18
- import type { CreditsPackage } from "../../domain/entities/CreditsPackage";
17
+ import type { PaywallTabType } from "../../../domain/entities/paywall/PaywallTab";
18
+ import type { CreditsPackage } from "../../../domain/entities/paywall/CreditsPackage";
19
19
 
20
20
  interface PaywallModalProps {
21
21
  visible: boolean;
@@ -27,10 +27,16 @@ interface PaywallModalProps {
27
27
  requiredCredits?: number;
28
28
  onCreditsPurchase: (packageId: string) => Promise<void>;
29
29
  onSubscriptionPurchase: (pkg: PurchasesPackage) => Promise<void>;
30
+ onRestore?: () => Promise<void>;
30
31
  subscriptionFeatures?: Array<{ icon: string; text: string }>;
31
32
  isLoading?: boolean;
32
33
  title?: string;
33
34
  subtitle?: string;
35
+ privacyUrl?: string;
36
+ termsUrl?: string;
37
+ privacyText?: string;
38
+ termsOfServiceText?: string;
39
+ restoreButtonText?: string;
34
40
  }
35
41
 
36
42
  export const PaywallModal: React.FC<PaywallModalProps> = React.memo(
@@ -44,10 +50,16 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo(
44
50
  requiredCredits,
45
51
  onCreditsPurchase,
46
52
  onSubscriptionPurchase,
53
+ onRestore,
47
54
  subscriptionFeatures = [],
48
55
  isLoading = false,
49
56
  title,
50
57
  subtitle,
58
+ privacyUrl,
59
+ termsUrl,
60
+ privacyText,
61
+ termsOfServiceText,
62
+ restoreButtonText,
51
63
  }) => {
52
64
  const tokens = useAppDesignTokens();
53
65
  const { t } = useLocalization();
@@ -101,12 +113,12 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo(
101
113
  onClose={onClose}
102
114
  />
103
115
 
104
- <PaywallTabBar
105
- activeTab={activeTab}
106
- onTabChange={handleTabChange}
107
- creditsLabel={t("paywall.tabs.credits", { defaultValue: "Credits" })}
108
- subscriptionLabel={t("paywall.tabs.subscription", { defaultValue: "Subscription" })}
109
- />
116
+ <PaywallTabBar
117
+ activeTab={activeTab}
118
+ onTabChange={handleTabChange}
119
+ creditsLabel={t("paywall.tabs.credits", { defaultValue: "Credits" })}
120
+ subscriptionLabel={t("paywall.tabs.subscription", { defaultValue: "Subscription" })}
121
+ />
110
122
 
111
123
  {activeTab === "credits" ? (
112
124
  <CreditsTabContent
@@ -128,6 +140,12 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo(
128
140
  features={subscriptionFeatures}
129
141
  isLoading={isLoading}
130
142
  purchaseButtonText={t("paywall.subscribe", { defaultValue: "Subscribe" })}
143
+ onRestore={onRestore}
144
+ privacyUrl={privacyUrl}
145
+ termsUrl={termsUrl}
146
+ privacyText={privacyText}
147
+ termsOfServiceText={termsOfServiceText}
148
+ restoreButtonText={restoreButtonText}
131
149
  />
132
150
  )}
133
151
  </SafeAreaView>
@@ -7,7 +7,7 @@ import React from "react";
7
7
  import { View, TouchableOpacity, StyleSheet } from "react-native";
8
8
  import { AtomicText } from "@umituz/react-native-design-system-atoms";
9
9
  import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
10
- import type { PaywallTabType } from "../../domain/entities/PaywallTab";
10
+ import type { PaywallTabType } from "../../../domain/entities/paywall/PaywallTab";
11
11
 
12
12
  interface PaywallTabBarProps {
13
13
  activeTab: PaywallTabType;
@@ -0,0 +1,90 @@
1
+ import React from "react";
2
+ import { View, StyleSheet, TouchableOpacity } from "react-native";
3
+ import { AtomicButton, AtomicText } from "@umituz/react-native-design-system-atoms";
4
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
5
+ import { PaywallLegalFooter } from "./PaywallLegalFooter";
6
+
7
+ interface SubscriptionFooterProps {
8
+ isProcessing: boolean;
9
+ isLoading: boolean;
10
+ processingText: string;
11
+ purchaseButtonText: string;
12
+ hasPackages: boolean;
13
+ selectedPkg: any; // Using any to avoid circular deps if needed, but preferably strict
14
+ restoreButtonText: string;
15
+ showRestoreButton: boolean;
16
+ privacyUrl?: string;
17
+ termsUrl?: string;
18
+ privacyText?: string;
19
+ termsOfServiceText?: string;
20
+ onPurchase: () => void;
21
+ onRestore: () => void;
22
+ }
23
+
24
+ export const SubscriptionFooter: React.FC<SubscriptionFooterProps> = React.memo(
25
+ ({
26
+ isProcessing,
27
+ isLoading,
28
+ processingText,
29
+ purchaseButtonText,
30
+ hasPackages,
31
+ selectedPkg,
32
+ restoreButtonText,
33
+ showRestoreButton,
34
+ privacyUrl,
35
+ termsUrl,
36
+ privacyText,
37
+ termsOfServiceText,
38
+ onPurchase,
39
+ onRestore,
40
+ }) => {
41
+ const tokens = useAppDesignTokens();
42
+
43
+ return (
44
+ <View style={styles.container}>
45
+ <View style={styles.actions}>
46
+ {hasPackages && (
47
+ <AtomicButton
48
+ title={isProcessing ? processingText : purchaseButtonText}
49
+ onPress={onPurchase}
50
+ disabled={!selectedPkg || isProcessing || isLoading}
51
+ />
52
+ )}
53
+ {showRestoreButton && (
54
+ <TouchableOpacity
55
+ style={styles.restoreButton}
56
+ onPress={onRestore}
57
+ disabled={isProcessing || isLoading}
58
+ >
59
+ <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
60
+ {restoreButtonText}
61
+ </AtomicText>
62
+ </TouchableOpacity>
63
+ )}
64
+ </View>
65
+
66
+ <PaywallLegalFooter
67
+ privacyUrl={privacyUrl}
68
+ termsUrl={termsUrl}
69
+ privacyText={privacyText}
70
+ termsOfServiceText={termsOfServiceText}
71
+ />
72
+ </View>
73
+ );
74
+ }
75
+ );
76
+
77
+ SubscriptionFooter.displayName = "SubscriptionFooter";
78
+
79
+ const styles = StyleSheet.create({
80
+ container: {},
81
+ actions: {
82
+ paddingHorizontal: 24,
83
+ paddingVertical: 16,
84
+ gap: 12
85
+ },
86
+ restoreButton: {
87
+ alignItems: "center",
88
+ paddingVertical: 8
89
+ },
90
+ });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Subscription Modal Component
3
- * Modal for displaying subscription packages
3
+ * Orchestrates subscription flow
4
4
  */
5
5
 
6
6
  import React, { useState, useCallback } from "react";
@@ -10,15 +10,14 @@ import {
10
10
  StyleSheet,
11
11
  TouchableOpacity,
12
12
  ScrollView,
13
- ActivityIndicator,
14
13
  } from "react-native";
15
14
  import { SafeAreaView } from "react-native-safe-area-context";
16
15
  import type { PurchasesPackage } from "react-native-purchases";
17
- import { AtomicText, AtomicButton } from "@umituz/react-native-design-system-atoms";
16
+ import { AtomicText } from "@umituz/react-native-design-system-atoms";
18
17
  import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
19
- import { SubscriptionPlanCard } from "./SubscriptionPlanCard";
20
18
  import { PaywallFeaturesList } from "./PaywallFeaturesList";
21
- import { PaywallLegalFooter } from "./PaywallLegalFooter";
19
+ import { SubscriptionPackageList } from "./SubscriptionPackageList";
20
+ import { SubscriptionFooter } from "./SubscriptionFooter";
22
21
 
23
22
  export interface SubscriptionModalProps {
24
23
  visible: boolean;
@@ -40,6 +39,8 @@ export interface SubscriptionModalProps {
40
39
  privacyText?: string;
41
40
  termsOfServiceText?: string;
42
41
  showRestoreButton?: boolean;
42
+ variant?: "bottom-sheet" | "fullscreen";
43
+ BackgroundComponent?: React.ComponentType<any>;
43
44
  }
44
45
 
45
46
  export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo(
@@ -63,6 +64,8 @@ export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo(
63
64
  privacyText,
64
65
  termsOfServiceText,
65
66
  showRestoreButton = true,
67
+ variant = "bottom-sheet",
68
+ BackgroundComponent,
66
69
  }) => {
67
70
  const tokens = useAppDesignTokens();
68
71
  const [selectedPkg, setSelectedPkg] = useState<PurchasesPackage | null>(null);
@@ -92,106 +95,92 @@ export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo(
92
95
 
93
96
  if (!visible) return null;
94
97
 
95
- const hasPackages = packages.length > 0;
96
- const showLoading = isLoading && !hasPackages;
98
+ const isFullScreen = variant === "fullscreen";
99
+
100
+ const Content = (
101
+ <SafeAreaView
102
+ edges={["top", "bottom"]}
103
+ style={isFullScreen ? styles.safeAreaFullScreen : styles.safeAreaBottomSheet}
104
+ >
105
+ <View style={styles.header}>
106
+ <TouchableOpacity style={styles.closeButton} onPress={onClose}>
107
+ <AtomicText style={[styles.closeIcon, { color: tokens.colors.textSecondary }]}>
108
+ ×
109
+ </AtomicText>
110
+ </TouchableOpacity>
111
+ <AtomicText
112
+ type="headlineMedium"
113
+ style={[styles.title, { color: tokens.colors.textPrimary }]}
114
+ >
115
+ {title}
116
+ </AtomicText>
117
+ {subtitle && (
118
+ <AtomicText
119
+ type="bodyMedium"
120
+ style={[styles.subtitle, { color: tokens.colors.textSecondary }]}
121
+ >
122
+ {subtitle}
123
+ </AtomicText>
124
+ )}
125
+ </View>
126
+
127
+ <ScrollView
128
+ style={isFullScreen ? styles.scrollViewFullScreen : styles.scrollViewBottomSheet}
129
+ contentContainerStyle={styles.scrollContent}
130
+ >
131
+ <SubscriptionPackageList
132
+ packages={packages}
133
+ isLoading={isLoading}
134
+ selectedPkg={selectedPkg}
135
+ onSelect={setSelectedPkg}
136
+ loadingText={loadingText}
137
+ emptyText={emptyText}
138
+ />
139
+ {features.length > 0 && (
140
+ <View style={[styles.featuresSection, { backgroundColor: tokens.colors.surfaceSecondary }]}>
141
+ <PaywallFeaturesList features={features} gap={12} />
142
+ </View>
143
+ )}
144
+ </ScrollView>
145
+
146
+ <SubscriptionFooter
147
+ isProcessing={isProcessing}
148
+ isLoading={isLoading}
149
+ processingText={processingText}
150
+ purchaseButtonText={purchaseButtonText}
151
+ hasPackages={packages.length > 0}
152
+ selectedPkg={selectedPkg}
153
+ restoreButtonText={restoreButtonText}
154
+ showRestoreButton={showRestoreButton}
155
+ privacyUrl={privacyUrl}
156
+ termsUrl={termsUrl}
157
+ privacyText={privacyText}
158
+ termsOfServiceText={termsOfServiceText}
159
+ onPurchase={handlePurchase}
160
+ onRestore={handleRestore}
161
+ />
162
+ </SafeAreaView>
163
+ );
164
+
165
+ if (variant === "fullscreen") {
166
+ const Wrapper = BackgroundComponent || View;
167
+ const wrapperStyle = !BackgroundComponent ? { flex: 1, backgroundColor: tokens.colors.backgroundPrimary } : { flex: 1 };
168
+
169
+ return (
170
+ <Modal visible={visible} transparent={false} animationType="slide" onRequestClose={onClose}>
171
+ <Wrapper style={wrapperStyle}>
172
+ {Content}
173
+ </Wrapper>
174
+ </Modal>
175
+ );
176
+ }
97
177
 
98
178
  return (
99
179
  <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
100
180
  <View style={styles.overlay}>
101
181
  <TouchableOpacity style={styles.backdrop} activeOpacity={1} onPress={onClose} />
102
- <View style={[styles.content, { backgroundColor: tokens.colors.surface }]}>
103
- <SafeAreaView edges={["bottom"]} style={styles.safeArea}>
104
- <View style={styles.header}>
105
- <TouchableOpacity style={styles.closeButton} onPress={onClose}>
106
- <AtomicText style={[styles.closeIcon, { color: tokens.colors.textSecondary }]}>
107
- ×
108
- </AtomicText>
109
- </TouchableOpacity>
110
- <AtomicText
111
- type="headlineMedium"
112
- style={[styles.title, { color: tokens.colors.textPrimary }]}
113
- >
114
- {title}
115
- </AtomicText>
116
- {subtitle ? (
117
- <AtomicText
118
- type="bodyMedium"
119
- style={[styles.subtitle, { color: tokens.colors.textSecondary }]}
120
- >
121
- {subtitle}
122
- </AtomicText>
123
- ) : null}
124
- </View>
125
-
126
- <ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}>
127
- {showLoading ? (
128
- <View style={styles.centerContent}>
129
- <ActivityIndicator size="large" color={tokens.colors.primary} />
130
- <AtomicText
131
- type="bodyMedium"
132
- style={[styles.loadingText, { color: tokens.colors.textSecondary }]}
133
- >
134
- {loadingText}
135
- </AtomicText>
136
- </View>
137
- ) : !hasPackages ? (
138
- <View style={styles.centerContent}>
139
- <AtomicText
140
- type="bodyMedium"
141
- style={[styles.emptyText, { color: tokens.colors.textSecondary }]}
142
- >
143
- {emptyText}
144
- </AtomicText>
145
- </View>
146
- ) : (
147
- <View style={styles.packagesContainer}>
148
- {packages.map((pkg, index) => (
149
- <SubscriptionPlanCard
150
- key={pkg.product.identifier}
151
- package={pkg}
152
- isSelected={selectedPkg?.product.identifier === pkg.product.identifier}
153
- onSelect={() => setSelectedPkg(pkg)}
154
- isBestValue={index === 0}
155
- />
156
- ))}
157
- </View>
158
- )}
159
-
160
- {features.length > 0 && (
161
- <View style={[styles.featuresSection, { backgroundColor: tokens.colors.surfaceSecondary }]}>
162
- <PaywallFeaturesList features={features} gap={12} />
163
- </View>
164
- )}
165
- </ScrollView>
166
-
167
- <View style={styles.footer}>
168
- {hasPackages && (
169
- <AtomicButton
170
- title={isProcessing ? processingText : purchaseButtonText}
171
- onPress={handlePurchase}
172
- disabled={!selectedPkg || isProcessing || isLoading}
173
- />
174
- )}
175
- {showRestoreButton && (
176
- <TouchableOpacity
177
- style={styles.restoreButton}
178
- onPress={handleRestore}
179
- disabled={isProcessing || isLoading}
180
- >
181
- <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
182
- {restoreButtonText}
183
- </AtomicText>
184
- </TouchableOpacity>
185
- )}
186
- </View>
187
-
188
- <PaywallLegalFooter
189
- privacyUrl={privacyUrl}
190
- termsUrl={termsUrl}
191
- privacyText={privacyText}
192
- termsOfServiceText={termsOfServiceText}
193
- />
194
- </SafeAreaView>
182
+ <View style={[styles.bottomSheetContent, { backgroundColor: tokens.colors.surface }]}>
183
+ {Content}
195
184
  </View>
196
185
  </View>
197
186
  </Modal>
@@ -204,20 +193,20 @@ SubscriptionModal.displayName = "SubscriptionModal";
204
193
  const styles = StyleSheet.create({
205
194
  overlay: { flex: 1, justifyContent: "flex-end" },
206
195
  backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: "rgba(0, 0, 0, 0.5)" },
207
- content: { borderTopLeftRadius: 24, borderTopRightRadius: 24, maxHeight: "90%" },
208
- safeArea: { paddingTop: 16 },
196
+ bottomSheetContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, maxHeight: "90%" },
197
+
198
+ safeAreaFullScreen: { flex: 1, paddingTop: 16 },
199
+ safeAreaBottomSheet: { paddingTop: 16 },
200
+
209
201
  header: { alignItems: "center", paddingHorizontal: 24, paddingBottom: 20 },
210
202
  closeButton: { position: "absolute", top: 0, right: 16, padding: 8, zIndex: 1 },
211
203
  closeIcon: { fontSize: 28, fontWeight: "300" },
212
204
  title: { marginBottom: 8, textAlign: "center" },
213
205
  subtitle: { textAlign: "center" },
214
- scrollView: { maxHeight: 400 },
206
+
207
+ scrollViewFullScreen: { flex: 1 },
208
+ scrollViewBottomSheet: { maxHeight: 400 },
209
+
215
210
  scrollContent: { paddingHorizontal: 24, paddingBottom: 16 },
216
- centerContent: { alignItems: "center", paddingVertical: 40 },
217
- loadingText: { marginTop: 16 },
218
- emptyText: { textAlign: "center" },
219
- packagesContainer: { gap: 12, marginBottom: 20 },
220
- featuresSection: { borderRadius: 16, padding: 16 },
221
- footer: { paddingHorizontal: 24, paddingVertical: 16, gap: 12 },
222
- restoreButton: { alignItems: "center", paddingVertical: 8 },
211
+ featuresSection: { borderRadius: 16, padding: 16, marginTop: 20 },
223
212
  });
@@ -0,0 +1,90 @@
1
+ import React from "react";
2
+ import { View, StyleSheet, ActivityIndicator } from "react-native";
3
+ import { AtomicText } from "@umituz/react-native-design-system-atoms";
4
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
5
+ import type { PurchasesPackage } from "react-native-purchases";
6
+ import { SubscriptionPlanCard } from "./SubscriptionPlanCard";
7
+
8
+ interface SubscriptionPackageListProps {
9
+ isLoading: boolean;
10
+ packages: PurchasesPackage[];
11
+ selectedPkg: PurchasesPackage | null;
12
+ loadingText: string;
13
+ emptyText: string;
14
+ onSelect: (pkg: PurchasesPackage) => void;
15
+ }
16
+
17
+ export const SubscriptionPackageList: React.FC<SubscriptionPackageListProps> = React.memo(
18
+ ({
19
+ isLoading,
20
+ packages,
21
+ selectedPkg,
22
+ loadingText,
23
+ emptyText,
24
+ onSelect,
25
+ }) => {
26
+ const tokens = useAppDesignTokens();
27
+ const hasPackages = packages.length > 0;
28
+ const showLoading = isLoading && !hasPackages;
29
+
30
+ if (showLoading) {
31
+ return (
32
+ <View style={styles.centerContent}>
33
+ <ActivityIndicator size="large" color={tokens.colors.primary} />
34
+ <AtomicText
35
+ type="bodyMedium"
36
+ style={[styles.loadingText, { color: tokens.colors.textSecondary }]}
37
+ >
38
+ {loadingText}
39
+ </AtomicText>
40
+ </View>
41
+ );
42
+ }
43
+
44
+ if (!hasPackages) {
45
+ return (
46
+ <View style={styles.centerContent}>
47
+ <AtomicText
48
+ type="bodyMedium"
49
+ style={[styles.emptyText, { color: tokens.colors.textSecondary }]}
50
+ >
51
+ {emptyText}
52
+ </AtomicText>
53
+ </View>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <View style={styles.packagesContainer}>
59
+ {packages.map((pkg, index) => (
60
+ <SubscriptionPlanCard
61
+ key={pkg.product.identifier}
62
+ package={pkg}
63
+ isSelected={selectedPkg?.product.identifier === pkg.product.identifier}
64
+ onSelect={() => onSelect(pkg)}
65
+ isBestValue={index === 0}
66
+ />
67
+ ))}
68
+ </View>
69
+ );
70
+ }
71
+ );
72
+
73
+ SubscriptionPackageList.displayName = "SubscriptionPackageList";
74
+
75
+ const styles = StyleSheet.create({
76
+ centerContent: {
77
+ alignItems: "center",
78
+ paddingVertical: 40
79
+ },
80
+ loadingText: {
81
+ marginTop: 16
82
+ },
83
+ emptyText: {
84
+ textAlign: "center"
85
+ },
86
+ packagesContainer: {
87
+ gap: 12,
88
+ marginBottom: 20
89
+ },
90
+ });
@@ -8,7 +8,7 @@ import { View, TouchableOpacity, StyleSheet } from "react-native";
8
8
  import type { PurchasesPackage } from "react-native-purchases";
9
9
  import { AtomicText } from "@umituz/react-native-design-system-atoms";
10
10
  import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
11
- import { formatPrice } from "@umituz/react-native-subscription";
11
+ import { formatPrice } from "../../../utils/priceUtils";
12
12
  import { useLocalization } from "@umituz/react-native-localization";
13
13
 
14
14
  interface SubscriptionPlanCardProps {
@@ -5,23 +5,28 @@
5
5
 
6
6
  import React, { useMemo } from "react";
7
7
  import { View, StyleSheet, ScrollView } from "react-native";
8
- import { AtomicButton } from "@umituz/react-native-design-system-atoms";
9
8
  import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
10
9
  import { useLocalization } from "@umituz/react-native-localization";
11
10
  import type { PurchasesPackage } from "react-native-purchases";
12
- import { SubscriptionPlanCard } from "./SubscriptionPlanCard";
13
11
  import { PaywallFeaturesList } from "./PaywallFeaturesList";
14
- import { PaywallLegalFooter } from "./PaywallLegalFooter";
12
+ import { SubscriptionPackageList } from "./SubscriptionPackageList";
13
+ import { SubscriptionFooter } from "./SubscriptionFooter";
15
14
 
16
15
  interface SubscriptionTabContentProps {
17
16
  packages: PurchasesPackage[];
18
17
  selectedPackage: PurchasesPackage | null;
19
18
  onSelectPackage: (pkg: PurchasesPackage) => void;
20
19
  onPurchase: () => void;
20
+ onRestore?: () => void;
21
21
  features?: Array<{ icon: string; text: string }>;
22
22
  isLoading?: boolean;
23
23
  purchaseButtonText?: string;
24
24
  processingText?: string;
25
+ restoreButtonText?: string;
26
+ privacyUrl?: string;
27
+ termsUrl?: string;
28
+ privacyText?: string;
29
+ termsOfServiceText?: string;
25
30
  }
26
31
 
27
32
  const isYearlyPackage = (pkg: PurchasesPackage): boolean => {
@@ -46,26 +51,27 @@ export const SubscriptionTabContent: React.FC<SubscriptionTabContentProps> =
46
51
  selectedPackage,
47
52
  onSelectPackage,
48
53
  onPurchase,
54
+ onRestore,
49
55
  features = [],
50
56
  isLoading = false,
51
57
  purchaseButtonText,
52
58
  processingText,
59
+ restoreButtonText = "Restore Purchases",
60
+ privacyUrl,
61
+ termsUrl,
62
+ privacyText,
63
+ termsOfServiceText,
53
64
  }) => {
54
65
  const tokens = useAppDesignTokens();
55
66
  const { t } = useLocalization();
56
67
 
57
- const displayPurchaseButtonText = purchaseButtonText ||
58
- t("paywall.subscribe", { defaultValue: "Subscribe" });
59
- const displayProcessingText = processingText ||
60
- t("paywall.processing", { defaultValue: "Processing..." });
68
+ const displayPurchaseButtonText =
69
+ purchaseButtonText || t("paywall.subscribe", { defaultValue: "Subscribe" });
70
+ const displayProcessingText =
71
+ processingText || t("paywall.processing", { defaultValue: "Processing..." });
61
72
 
62
73
  const sortedPackages = useMemo(() => sortPackages(packages), [packages]);
63
74
 
64
- const firstYearlyIndex = useMemo(
65
- () => sortedPackages.findIndex(isYearlyPackage),
66
- [sortedPackages],
67
- );
68
-
69
75
  return (
70
76
  <View style={styles.container}>
71
77
  <ScrollView
@@ -73,20 +79,14 @@ export const SubscriptionTabContent: React.FC<SubscriptionTabContentProps> =
73
79
  contentContainerStyle={styles.scrollContent}
74
80
  showsVerticalScrollIndicator={false}
75
81
  >
76
- <View style={styles.plansContainer}>
77
- {sortedPackages.map((pkg, index) => (
78
- <SubscriptionPlanCard
79
- key={pkg.product.identifier}
80
- package={pkg}
81
- isSelected={
82
- selectedPackage?.product.identifier ===
83
- pkg.product.identifier
84
- }
85
- onSelect={() => onSelectPackage(pkg)}
86
- isBestValue={index === firstYearlyIndex}
87
- />
88
- ))}
89
- </View>
82
+ <SubscriptionPackageList
83
+ packages={sortedPackages}
84
+ isLoading={isLoading}
85
+ selectedPkg={selectedPackage}
86
+ onSelect={onSelectPackage}
87
+ loadingText={t("paywall.loading", { defaultValue: "Loading..." })}
88
+ emptyText={t("paywall.empty", { defaultValue: "No packages" })}
89
+ />
90
90
 
91
91
  {features.length > 0 && (
92
92
  <View
@@ -100,18 +100,25 @@ export const SubscriptionTabContent: React.FC<SubscriptionTabContentProps> =
100
100
  )}
101
101
  </ScrollView>
102
102
 
103
- <View style={styles.footer}>
104
- <AtomicButton
105
- title={isLoading ? displayProcessingText : displayPurchaseButtonText}
106
- onPress={onPurchase}
107
- disabled={!selectedPackage || isLoading}
108
- />
109
- </View>
110
-
111
- <PaywallLegalFooter />
103
+ <SubscriptionFooter
104
+ isProcessing={false} // Tab content usually delegated processing to external state, currently no isProcessing prop passed. Assuming false or controlled by isLoading.
105
+ isLoading={isLoading}
106
+ processingText={displayProcessingText}
107
+ purchaseButtonText={displayPurchaseButtonText}
108
+ hasPackages={packages.length > 0}
109
+ selectedPkg={selectedPackage}
110
+ restoreButtonText={restoreButtonText}
111
+ showRestoreButton={!!onRestore}
112
+ onPurchase={onPurchase}
113
+ onRestore={onRestore || (() => { })}
114
+ privacyUrl={privacyUrl}
115
+ termsUrl={termsUrl}
116
+ privacyText={privacyText}
117
+ termsOfServiceText={termsOfServiceText}
118
+ />
112
119
  </View>
113
120
  );
114
- },
121
+ }
115
122
  );
116
123
 
117
124
  SubscriptionTabContent.displayName = "SubscriptionTabContent";
@@ -127,16 +134,9 @@ const styles = StyleSheet.create({
127
134
  paddingHorizontal: 24,
128
135
  paddingBottom: 16,
129
136
  },
130
- plansContainer: {
131
- gap: 12,
132
- marginBottom: 20,
133
- },
134
137
  featuresSection: {
135
138
  borderRadius: 16,
136
139
  padding: 16,
137
- },
138
- footer: {
139
- padding: 24,
140
- paddingTop: 16,
140
+ marginTop: 20,
141
141
  },
142
142
  });
@@ -0,0 +1,54 @@
1
+ import { useState, useCallback } from "react";
2
+ import type { PurchasesPackage } from "react-native-purchases";
3
+ import type { PaywallTabType } from "../../domain/entities/paywall/PaywallTab";
4
+
5
+ interface UsePaywallProps {
6
+ initialTab?: PaywallTabType;
7
+ onCreditsPurchase: (packageId: string) => Promise<void>;
8
+ onSubscriptionPurchase: (pkg: PurchasesPackage) => Promise<void>;
9
+ }
10
+
11
+ export const usePaywall = ({
12
+ initialTab = "credits",
13
+ onCreditsPurchase,
14
+ onSubscriptionPurchase,
15
+ }: UsePaywallProps) => {
16
+ const [activeTab, setActiveTab] = useState<PaywallTabType>(initialTab);
17
+ const [selectedCreditsPackageId, setSelectedCreditsPackageId] = useState<string | null>(null);
18
+ const [selectedSubscriptionPkg, setSelectedSubscriptionPkg] = useState<PurchasesPackage | null>(null);
19
+
20
+ const handleTabChange = useCallback((tab: PaywallTabType) => {
21
+ setActiveTab(tab);
22
+ }, []);
23
+
24
+ const handleCreditsPackageSelect = useCallback((packageId: string) => {
25
+ setSelectedCreditsPackageId(packageId);
26
+ }, []);
27
+
28
+ const handleSubscriptionPackageSelect = useCallback((pkg: PurchasesPackage) => {
29
+ setSelectedSubscriptionPkg(pkg);
30
+ }, []);
31
+
32
+ const handleCreditsPurchase = useCallback(async () => {
33
+ if (selectedCreditsPackageId) {
34
+ await onCreditsPurchase(selectedCreditsPackageId);
35
+ }
36
+ }, [selectedCreditsPackageId, onCreditsPurchase]);
37
+
38
+ const handleSubscriptionPurchase = useCallback(async () => {
39
+ if (selectedSubscriptionPkg) {
40
+ await onSubscriptionPurchase(selectedSubscriptionPkg);
41
+ }
42
+ }, [selectedSubscriptionPkg, onSubscriptionPurchase]);
43
+
44
+ return {
45
+ activeTab,
46
+ selectedCreditsPackageId,
47
+ selectedSubscriptionPkg,
48
+ handleTabChange,
49
+ handleCreditsPackageSelect,
50
+ handleSubscriptionPackageSelect,
51
+ handleCreditsPurchase,
52
+ handleSubscriptionPurchase,
53
+ };
54
+ };