@umituz/react-native-subscription 2.2.20 → 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.20",
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";
@@ -34,7 +34,7 @@ export const PaywallFeatureItem: React.FC<PaywallFeatureItemProps> = React.memo(
34
34
  />
35
35
  <AtomicText
36
36
  type="bodyMedium"
37
- style={[styles.featureText, { color: tokens.colors.text }]}
37
+ style={[styles.featureText, { color: tokens.colors.textPrimary }]}
38
38
  >
39
39
  {text}
40
40
  </AtomicText>
@@ -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) => (
@@ -92,25 +92,28 @@ const styles = StyleSheet.create({
92
92
  container: {
93
93
  alignItems: "center",
94
94
  paddingHorizontal: 24,
95
- paddingBottom: 20,
95
+ paddingBottom: 32, // Increased for home indicator safety
96
96
  paddingTop: 8,
97
+ width: "100%",
97
98
  },
98
99
  termsText: {
99
100
  textAlign: "center",
100
101
  fontSize: 11,
101
102
  lineHeight: 16,
102
- marginBottom: 8,
103
+ marginBottom: 10,
103
104
  },
104
105
  legalLinksContainer: {
105
106
  flexDirection: "row",
106
107
  alignItems: "center",
108
+ justifyContent: "center",
107
109
  marginTop: 4,
110
+ flexWrap: "wrap",
108
111
  },
109
112
  linkText: {
110
113
  textDecorationLine: "underline",
111
114
  fontSize: 12,
112
115
  },
113
116
  separator: {
114
- marginHorizontal: 4,
117
+ marginHorizontal: 8,
115
118
  },
116
119
  });
@@ -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,215 +61,111 @@ 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: {
155
+ container: {
217
156
  flex: 1,
218
- justifyContent: "flex-end",
219
157
  },
220
- backdrop: {
221
- ...StyleSheet.absoluteFillObject,
222
- backgroundColor: "rgba(0, 0, 0, 0.5)",
223
- },
224
-
225
- // Bottom-sheet container - KEY FIX: explicit max height calculation
226
- bottomSheetContainer: {
227
- borderTopLeftRadius: 24,
228
- borderTopRightRadius: 24,
229
- maxHeight: SCREEN_HEIGHT * 0.85,
230
- paddingTop: 16,
231
- paddingBottom: 24,
232
- },
233
-
234
- // Fullscreen container
235
- fullscreenContainer: {
236
- flex: 1,
237
- paddingTop: 16,
238
- },
239
-
240
- // Header
241
- header: {
242
- alignItems: "center",
243
- paddingHorizontal: 24,
244
- paddingBottom: 16,
245
- },
246
- closeButton: {
247
- position: "absolute",
248
- top: 0,
249
- right: 16,
250
- padding: 8,
251
- zIndex: 1,
252
- },
253
- closeIcon: {
254
- fontSize: 28,
255
- fontWeight: "300",
256
- },
257
- title: {
258
- marginBottom: 8,
259
- textAlign: "center",
260
- },
261
- subtitle: {
262
- textAlign: "center",
263
- },
264
-
265
- // ScrollView - expands to fill available space, shrinks if needed
266
158
  scrollView: {
267
159
  flex: 1,
268
- minHeight: 100,
269
160
  },
270
161
  scrollContent: {
271
162
  paddingHorizontal: 24,
272
- paddingBottom: 16,
163
+ paddingBottom: 24,
273
164
  flexGrow: 1,
274
165
  },
275
-
276
- // Features section
277
166
  featuresSection: {
278
- borderRadius: 16,
279
- padding: 16,
280
- marginTop: 16,
167
+ borderRadius: 20,
168
+ padding: 20,
169
+ marginTop: 8,
281
170
  },
282
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
+ });