@umituz/react-native-subscription 2.2.21 → 2.2.22

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.21",
3
+ "version": "2.2.22",
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",
package/src/index.ts CHANGED
@@ -96,6 +96,9 @@ export {
96
96
  type SubscriptionModalProps,
97
97
  } from "./presentation/components/paywall/SubscriptionModal";
98
98
 
99
+ export { SubscriptionModalHeader } from "./presentation/components/paywall/SubscriptionModalHeader";
100
+ export { SubscriptionModalOverlay, type SubscriptionModalVariant } from "./presentation/components/paywall/SubscriptionModalOverlay";
101
+
99
102
  export { SubscriptionPlanCard } from "./presentation/components/paywall/SubscriptionPlanCard";
100
103
  export { PaywallFeaturesList } from "./presentation/components/paywall/PaywallFeaturesList";
101
104
  export { PaywallFeatureItem } from "./presentation/components/paywall/PaywallFeatureItem";
@@ -18,6 +18,9 @@ interface PaywallFeaturesListProps {
18
18
 
19
19
  export const PaywallFeaturesList: React.FC<PaywallFeaturesListProps> = React.memo(
20
20
  ({ features, containerStyle, gap = 12 }) => {
21
+ if (__DEV__) {
22
+ console.log("[PaywallFeaturesList] Rendering features count:", features.length);
23
+ }
21
24
  return (
22
25
  <View style={[styles.container, containerStyle]}>
23
26
  {features.map((feature, index) => (
@@ -1,27 +1,20 @@
1
1
  /**
2
2
  * Subscription Modal Component
3
- * Orchestrates subscription flow
3
+ * Orchestrates subscription flow using decomposed components
4
4
  */
5
5
 
6
6
  import React, { useState, useCallback } from "react";
7
- import {
8
- View,
9
- Modal,
10
- StyleSheet,
11
- TouchableOpacity,
12
- ScrollView,
13
- Dimensions,
14
- } from "react-native";
7
+ import { View, StyleSheet, ScrollView } from "react-native";
15
8
  import { SafeAreaView } from "react-native-safe-area-context";
16
- import type { PurchasesPackage } from "react-native-purchases";
17
- import { AtomicText } from "@umituz/react-native-design-system-atoms";
18
9
  import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
10
+ import type { PurchasesPackage } from "react-native-purchases";
11
+
12
+ import { SubscriptionModalHeader } from "./SubscriptionModalHeader";
13
+ import { SubscriptionModalOverlay, SubscriptionModalVariant } from "./SubscriptionModalOverlay";
19
14
  import { PaywallFeaturesList } from "./PaywallFeaturesList";
20
15
  import { SubscriptionPackageList } from "./SubscriptionPackageList";
21
16
  import { SubscriptionFooter } from "./SubscriptionFooter";
22
17
 
23
- const { height: SCREEN_HEIGHT } = Dimensions.get("window");
24
-
25
18
  export interface SubscriptionModalProps {
26
19
  visible: boolean;
27
20
  onClose: () => void;
@@ -42,12 +35,12 @@ export interface SubscriptionModalProps {
42
35
  privacyText?: string;
43
36
  termsOfServiceText?: string;
44
37
  showRestoreButton?: boolean;
45
- variant?: "bottom-sheet" | "fullscreen";
38
+ variant?: SubscriptionModalVariant;
46
39
  BackgroundComponent?: React.ComponentType<any>;
47
40
  }
48
41
 
49
- export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo(
50
- ({
42
+ export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo((props) => {
43
+ const {
51
44
  visible,
52
45
  onClose,
53
46
  packages,
@@ -59,8 +52,8 @@ export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo(
59
52
  isLoading = false,
60
53
  purchaseButtonText = "Subscribe",
61
54
  restoreButtonText = "Restore Purchases",
62
- loadingText = "Loading packages...",
63
- emptyText = "No packages available",
55
+ loadingText = "Loading...",
56
+ emptyText = "No packages",
64
57
  processingText = "Processing...",
65
58
  privacyUrl,
66
59
  termsUrl,
@@ -68,204 +61,100 @@ export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo(
68
61
  termsOfServiceText,
69
62
  showRestoreButton = true,
70
63
  variant = "bottom-sheet",
71
- BackgroundComponent,
72
- }) => {
73
- const tokens = useAppDesignTokens();
74
- const [selectedPkg, setSelectedPkg] = useState<PurchasesPackage | null>(null);
75
- const [isProcessing, setIsProcessing] = useState(false);
76
-
77
- const handlePurchase = useCallback(async () => {
78
- if (!selectedPkg || isProcessing) return;
79
- setIsProcessing(true);
80
- try {
81
- const success = await onPurchase(selectedPkg);
82
- if (success) onClose();
83
- } finally {
84
- setIsProcessing(false);
85
- }
86
- }, [selectedPkg, isProcessing, onPurchase, onClose]);
64
+ } = props;
65
+
66
+ const tokens = useAppDesignTokens();
67
+ const [selectedPkg, setSelectedPkg] = useState<PurchasesPackage | null>(null);
68
+ const [isProcessing, setIsProcessing] = useState(false);
69
+
70
+ if (__DEV__ && visible) {
71
+ console.log("[SubscriptionModal] Props Info:", {
72
+ packagesCount: packages.length,
73
+ featuresCount: features.length,
74
+ variant,
75
+ hasPrivacyUrl: !!privacyUrl,
76
+ });
77
+ }
87
78
 
88
- const handleRestore = useCallback(async () => {
89
- if (isProcessing) return;
90
- setIsProcessing(true);
91
- try {
92
- const success = await onRestore();
93
- if (success) onClose();
94
- } finally {
95
- setIsProcessing(false);
96
- }
97
- }, [isProcessing, onRestore, onClose]);
79
+ const handlePurchase = useCallback(async () => {
80
+ if (!selectedPkg || isProcessing) return;
81
+ setIsProcessing(true);
82
+ try {
83
+ if (await onPurchase(selectedPkg)) onClose();
84
+ } finally {
85
+ setIsProcessing(false);
86
+ }
87
+ }, [selectedPkg, isProcessing, onPurchase, onClose]);
88
+
89
+ const handleRestore = useCallback(async () => {
90
+ if (isProcessing) return;
91
+ setIsProcessing(true);
92
+ try {
93
+ if (await onRestore()) onClose();
94
+ } finally {
95
+ setIsProcessing(false);
96
+ }
97
+ }, [isProcessing, onRestore, onClose]);
98
98
 
99
- if (!visible) return null;
99
+ if (!visible) return null;
100
100
 
101
- const isFullScreen = variant === "fullscreen";
101
+ const ContentWrapper = variant === "fullscreen" ? SafeAreaView : View;
102
+ const wrapperProps = variant === "fullscreen" ? { edges: ["top", "bottom"] as const } : {};
102
103
 
103
- // Fullscreen variant uses SafeAreaView, bottom-sheet uses regular View
104
- const ContentWrapper = isFullScreen ? SafeAreaView : View;
105
- const wrapperProps = isFullScreen ? { edges: ["top", "bottom"] as const } : {};
104
+ return (
105
+ <SubscriptionModalOverlay visible={visible} onClose={onClose} variant={variant}>
106
+ <ContentWrapper {...wrapperProps} style={styles.container}>
107
+ <SubscriptionModalHeader title={title} subtitle={subtitle} onClose={onClose} />
106
108
 
107
- // Scrollable content (packages + features)
108
- const ScrollContent = (
109
- <ScrollView
110
- style={styles.scrollView}
111
- contentContainerStyle={styles.scrollContent}
112
- showsVerticalScrollIndicator={false}
113
- bounces={false}
114
- >
115
- <SubscriptionPackageList
116
- packages={packages}
109
+ <ScrollView
110
+ style={styles.scrollView}
111
+ contentContainerStyle={styles.scrollContent}
112
+ showsVerticalScrollIndicator={false}
113
+ bounces={false}
114
+ >
115
+ <SubscriptionPackageList
116
+ packages={packages}
117
+ isLoading={isLoading}
118
+ selectedPkg={selectedPkg}
119
+ onSelect={setSelectedPkg}
120
+ loadingText={loadingText}
121
+ emptyText={emptyText}
122
+ />
123
+
124
+ {features.length > 0 && (
125
+ <View style={[styles.featuresSection, { backgroundColor: tokens.colors.surfaceSecondary }]}>
126
+ <PaywallFeaturesList features={features} gap={12} />
127
+ </View>
128
+ )}
129
+ </ScrollView>
130
+
131
+ <SubscriptionFooter
132
+ isProcessing={isProcessing}
117
133
  isLoading={isLoading}
134
+ processingText={processingText}
135
+ purchaseButtonText={purchaseButtonText}
136
+ hasPackages={packages.length > 0}
118
137
  selectedPkg={selectedPkg}
119
- onSelect={setSelectedPkg}
120
- loadingText={loadingText}
121
- emptyText={emptyText}
138
+ restoreButtonText={restoreButtonText}
139
+ showRestoreButton={showRestoreButton}
140
+ privacyUrl={privacyUrl}
141
+ termsUrl={termsUrl}
142
+ privacyText={privacyText}
143
+ termsOfServiceText={termsOfServiceText}
144
+ onPurchase={handlePurchase}
145
+ onRestore={handleRestore}
122
146
  />
123
- {features.length > 0 && (
124
- <View style={[styles.featuresSection, { backgroundColor: tokens.colors.surfaceSecondary }]}>
125
- <PaywallFeaturesList features={features} gap={12} />
126
- </View>
127
- )}
128
- </ScrollView>
129
- );
130
-
131
- // Footer with buttons and legal links
132
- const Footer = (
133
- <SubscriptionFooter
134
- isProcessing={isProcessing}
135
- isLoading={isLoading}
136
- processingText={processingText}
137
- purchaseButtonText={purchaseButtonText}
138
- hasPackages={packages.length > 0}
139
- selectedPkg={selectedPkg}
140
- restoreButtonText={restoreButtonText}
141
- showRestoreButton={showRestoreButton}
142
- privacyUrl={privacyUrl}
143
- termsUrl={termsUrl}
144
- privacyText={privacyText}
145
- termsOfServiceText={termsOfServiceText}
146
- onPurchase={handlePurchase}
147
- onRestore={handleRestore}
148
- />
149
- );
150
-
151
- // Header with title and close button
152
- const Header = (
153
- <View style={styles.header}>
154
- <TouchableOpacity style={styles.closeButton} onPress={onClose}>
155
- <AtomicText style={[styles.closeIcon, { color: tokens.colors.textSecondary }]}>
156
- ×
157
- </AtomicText>
158
- </TouchableOpacity>
159
- <AtomicText
160
- type="headlineMedium"
161
- style={[styles.title, { color: tokens.colors.textPrimary }]}
162
- >
163
- {title}
164
- </AtomicText>
165
- {subtitle && (
166
- <AtomicText
167
- type="bodyMedium"
168
- style={[styles.subtitle, { color: tokens.colors.textSecondary }]}
169
- >
170
- {subtitle}
171
- </AtomicText>
172
- )}
173
- </View>
174
- );
175
-
176
- // FULLSCREEN VARIANT
177
- if (isFullScreen) {
178
- const Wrapper = BackgroundComponent || View;
179
- const wrapperStyle = !BackgroundComponent
180
- ? { flex: 1, backgroundColor: tokens.colors.backgroundPrimary }
181
- : { flex: 1 };
182
-
183
- return (
184
- <Modal visible={visible} transparent={false} animationType="slide" onRequestClose={onClose}>
185
- <Wrapper style={wrapperStyle}>
186
- <ContentWrapper {...wrapperProps} style={styles.fullscreenContainer}>
187
- {Header}
188
- {ScrollContent}
189
- {Footer}
190
- </ContentWrapper>
191
- </Wrapper>
192
- </Modal>
193
- );
194
- }
195
-
196
- // BOTTOM-SHEET VARIANT
197
- return (
198
- <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
199
- <View style={styles.overlay}>
200
- <TouchableOpacity style={styles.backdrop} activeOpacity={1} onPress={onClose} />
201
- <View style={[styles.bottomSheetContainer, { backgroundColor: tokens.colors.surface }]}>
202
- {Header}
203
- {ScrollContent}
204
- {Footer}
205
- </View>
206
- </View>
207
- </Modal>
208
- );
209
- }
210
- );
147
+ </ContentWrapper>
148
+ </SubscriptionModalOverlay>
149
+ );
150
+ });
211
151
 
212
152
  SubscriptionModal.displayName = "SubscriptionModal";
213
153
 
214
154
  const styles = StyleSheet.create({
215
- // Overlay and backdrop for bottom-sheet
216
- overlay: {
217
- flex: 1,
218
- justifyContent: "flex-end",
219
- },
220
- backdrop: {
221
- ...StyleSheet.absoluteFillObject,
222
- backgroundColor: "rgba(0, 0, 0, 0.5)",
223
- },
224
-
225
- // Bottom-sheet container
226
- bottomSheetContainer: {
227
- borderTopLeftRadius: 32,
228
- borderTopRightRadius: 32,
229
- maxHeight: SCREEN_HEIGHT * 0.9,
230
- minHeight: 400,
231
- width: "100%",
232
- paddingBottom: 0, // Footer has its own padding
233
- },
234
-
235
- // Fullscreen container
236
- fullscreenContainer: {
155
+ container: {
237
156
  flex: 1,
238
- paddingTop: 16,
239
- },
240
-
241
- // Header
242
- header: {
243
- alignItems: "center",
244
- paddingHorizontal: 24,
245
- paddingTop: 8,
246
- paddingBottom: 16,
247
- },
248
- closeButton: {
249
- position: "absolute",
250
- top: 8,
251
- right: 16,
252
- padding: 8,
253
- zIndex: 1,
254
- },
255
- closeIcon: {
256
- fontSize: 28,
257
- fontWeight: "300",
258
157
  },
259
- title: {
260
- marginBottom: 8,
261
- textAlign: "center",
262
- },
263
- subtitle: {
264
- textAlign: "center",
265
- paddingHorizontal: 20,
266
- },
267
-
268
- // ScrollView - expands to fill available space, shrinks if needed
269
158
  scrollView: {
270
159
  flex: 1,
271
160
  },
@@ -274,13 +163,9 @@ const styles = StyleSheet.create({
274
163
  paddingBottom: 24,
275
164
  flexGrow: 1,
276
165
  },
277
-
278
- // Features section
279
166
  featuresSection: {
280
167
  borderRadius: 20,
281
168
  padding: 20,
282
169
  marginTop: 8,
283
- borderWidth: 1,
284
- borderColor: "rgba(255, 255, 255, 0.05)",
285
170
  },
286
171
  });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Subscription Modal Header Component
3
+ */
4
+
5
+ import React from "react";
6
+ import { View, StyleSheet, TouchableOpacity } from "react-native";
7
+ import { AtomicText } from "@umituz/react-native-design-system-atoms";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
9
+
10
+ interface SubscriptionModalHeaderProps {
11
+ title: string;
12
+ subtitle?: string;
13
+ onClose: () => void;
14
+ }
15
+
16
+ export const SubscriptionModalHeader: React.FC<SubscriptionModalHeaderProps> = ({
17
+ title,
18
+ subtitle,
19
+ onClose,
20
+ }) => {
21
+ const tokens = useAppDesignTokens();
22
+
23
+ if (__DEV__) {
24
+ console.log("[SubscriptionModalHeader] Rendering title:", title);
25
+ }
26
+
27
+ return (
28
+ <View style={styles.header}>
29
+ <TouchableOpacity
30
+ style={styles.closeButton}
31
+ onPress={onClose}
32
+ testID="subscription-modal-close-button"
33
+ >
34
+ <AtomicText style={[styles.closeIcon, { color: tokens.colors.textSecondary }]}>
35
+ ×
36
+ </AtomicText>
37
+ </TouchableOpacity>
38
+ <AtomicText
39
+ type="headlineMedium"
40
+ style={[styles.title, { color: tokens.colors.textPrimary }]}
41
+ >
42
+ {title}
43
+ </AtomicText>
44
+ {subtitle && (
45
+ <AtomicText
46
+ type="bodyMedium"
47
+ style={[styles.subtitle, { color: tokens.colors.textSecondary }]}
48
+ >
49
+ {subtitle}
50
+ </AtomicText>
51
+ )}
52
+ </View>
53
+ );
54
+ };
55
+
56
+ const styles = StyleSheet.create({
57
+ header: {
58
+ alignItems: "center",
59
+ paddingHorizontal: 24,
60
+ paddingTop: 8,
61
+ paddingBottom: 16,
62
+ },
63
+ closeButton: {
64
+ position: "absolute",
65
+ top: 8,
66
+ right: 16,
67
+ padding: 8,
68
+ zIndex: 1,
69
+ },
70
+ closeIcon: {
71
+ fontSize: 28,
72
+ fontWeight: "300",
73
+ },
74
+ title: {
75
+ marginBottom: 8,
76
+ textAlign: "center",
77
+ },
78
+ subtitle: {
79
+ textAlign: "center",
80
+ paddingHorizontal: 20,
81
+ },
82
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Subscription Modal Overlay Component
3
+ * Handles the Modal wrapper and backdrop logic for different variants
4
+ */
5
+
6
+ import React from "react";
7
+ import {
8
+ View,
9
+ Modal,
10
+ StyleSheet,
11
+ TouchableOpacity,
12
+ Dimensions,
13
+ Platform
14
+ } from "react-native";
15
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
16
+
17
+ const { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = Dimensions.get("window");
18
+
19
+ export type SubscriptionModalVariant = "bottom-sheet" | "fullscreen" | "dialog";
20
+
21
+ interface SubscriptionModalOverlayProps {
22
+ visible: boolean;
23
+ onClose: () => void;
24
+ children: React.ReactNode;
25
+ variant: SubscriptionModalVariant;
26
+ }
27
+
28
+ export const SubscriptionModalOverlay: React.FC<SubscriptionModalOverlayProps> = ({
29
+ visible,
30
+ onClose,
31
+ children,
32
+ variant,
33
+ }) => {
34
+ const tokens = useAppDesignTokens();
35
+
36
+ if (__DEV__) {
37
+ console.log("[SubscriptionModalOverlay] Rendering variant:", variant);
38
+ }
39
+
40
+ const isFullScreen = variant === "fullscreen";
41
+
42
+ if (isFullScreen) {
43
+ return (
44
+ <Modal
45
+ visible={visible}
46
+ transparent={false}
47
+ animationType="slide"
48
+ onRequestClose={onClose}
49
+ >
50
+ <View style={[styles.fullscreen, { backgroundColor: tokens.colors.backgroundPrimary }]}>
51
+ {children}
52
+ </View>
53
+ </Modal>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <Modal
59
+ visible={visible}
60
+ transparent
61
+ animationType="fade"
62
+ onRequestClose={onClose}
63
+ >
64
+ <View style={styles.overlay}>
65
+ <TouchableOpacity
66
+ style={styles.backdrop}
67
+ activeOpacity={1}
68
+ onPress={onClose}
69
+ />
70
+ <View
71
+ style={[
72
+ variant === "bottom-sheet" ? styles.bottomSheet : styles.dialog,
73
+ { backgroundColor: tokens.colors.surface }
74
+ ]}
75
+ >
76
+ {children}
77
+ </View>
78
+ </View>
79
+ </Modal>
80
+ );
81
+ };
82
+
83
+ const styles = StyleSheet.create({
84
+ overlay: {
85
+ flex: 1,
86
+ justifyContent: "flex-end", // Bottom-sheet default
87
+ },
88
+ backdrop: {
89
+ ...StyleSheet.absoluteFillObject,
90
+ backgroundColor: "rgba(0, 0, 0, 0.6)",
91
+ },
92
+ fullscreen: {
93
+ flex: 1,
94
+ },
95
+ bottomSheet: {
96
+ borderTopLeftRadius: 32,
97
+ borderTopRightRadius: 32,
98
+ maxHeight: SCREEN_HEIGHT * 0.9,
99
+ minHeight: 400,
100
+ width: "100%",
101
+ },
102
+ dialog: {
103
+ alignSelf: "center",
104
+ marginBottom: "auto",
105
+ marginTop: "auto",
106
+ width: Math.min(SCREEN_WIDTH * 0.9, 450),
107
+ maxHeight: SCREEN_HEIGHT * 0.8,
108
+ borderRadius: 24,
109
+ overflow: "hidden",
110
+ ...Platform.select({
111
+ ios: {
112
+ shadowColor: "#000",
113
+ shadowOffset: { width: 0, height: 10 },
114
+ shadowOpacity: 0.3,
115
+ shadowRadius: 20,
116
+ },
117
+ android: {
118
+ elevation: 10,
119
+ },
120
+ }),
121
+ },
122
+ });