@umituz/react-native-subscription 2.2.21 → 2.2.23
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 +17 -12
- package/src/index.ts +3 -0
- package/src/presentation/components/paywall/PaywallFeaturesList.tsx +3 -0
- package/src/presentation/components/paywall/SubscriptionModal.tsx +91 -206
- package/src/presentation/components/paywall/SubscriptionModalHeader.tsx +82 -0
- package/src/presentation/components/paywall/SubscriptionModalOverlay.tsx +122 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.23",
|
|
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",
|
|
@@ -27,28 +27,33 @@
|
|
|
27
27
|
"url": "git+https://github.com/umituz/react-native-subscription.git"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"react": ">=18.2.0",
|
|
31
|
-
"react-native": ">=0.74.0",
|
|
32
|
-
"react-native-purchases": ">=8.0.0",
|
|
33
|
-
"react-native-safe-area-context": ">=4.0.0",
|
|
34
30
|
"@tanstack/react-query": ">=5.0.0",
|
|
35
|
-
"@umituz/react-native-firestore": "*",
|
|
36
31
|
"@umituz/react-native-design-system-atoms": "*",
|
|
37
32
|
"@umituz/react-native-design-system-theme": "*",
|
|
33
|
+
"@umituz/react-native-firestore": "*",
|
|
38
34
|
"@umituz/react-native-legal": "*",
|
|
35
|
+
"expo-constants": ">=18.0.0",
|
|
39
36
|
"firebase": ">=10.0.0",
|
|
40
|
-
"
|
|
37
|
+
"react": ">=18.2.0",
|
|
38
|
+
"react-native": ">=0.74.0",
|
|
39
|
+
"react-native-purchases": ">=8.0.0",
|
|
40
|
+
"react-native-safe-area-context": ">=4.0.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
+
"@tanstack/react-query": "^5.0.0",
|
|
44
|
+
"@types/node": "^25.0.3",
|
|
43
45
|
"@types/react": "~19.1.0",
|
|
44
|
-
"
|
|
45
|
-
"react-native": "~0.76.0",
|
|
46
|
+
"@umituz/react-native-design-system-atoms": "^1.19.0",
|
|
46
47
|
"@umituz/react-native-design-system-theme": "latest",
|
|
47
48
|
"@umituz/react-native-firestore": "latest",
|
|
48
|
-
"@
|
|
49
|
-
"
|
|
49
|
+
"@umituz/react-native-legal": "^2.1.2",
|
|
50
|
+
"@umituz/react-native-localization": "^3.5.8",
|
|
50
51
|
"expo-constants": "~18.0.0",
|
|
51
|
-
"
|
|
52
|
+
"firebase": "^11.0.0",
|
|
53
|
+
"react-native": "~0.76.0",
|
|
54
|
+
"react-native-purchases": "~9.6.0",
|
|
55
|
+
"react-native-safe-area-context": "^5.6.2",
|
|
56
|
+
"typescript": "~5.9.2"
|
|
52
57
|
},
|
|
53
58
|
"publishConfig": {
|
|
54
59
|
"access": "public"
|
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?:
|
|
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
|
|
63
|
-
emptyText = "No packages
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
99
|
+
if (!visible) return null;
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
const ContentWrapper = variant === "fullscreen" ? SafeAreaView : View;
|
|
102
|
+
const wrapperProps = variant === "fullscreen" ? { edges: ["top", "bottom"] as const } : {};
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
+
});
|