@umituz/react-native-subscription 2.2.7 → 2.2.8
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.
|
|
3
|
+
"version": "2.2.8",
|
|
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,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
|
-
*
|
|
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
|
|
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 {
|
|
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,84 @@ export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo(
|
|
|
92
95
|
|
|
93
96
|
if (!visible) return null;
|
|
94
97
|
|
|
95
|
-
const
|
|
96
|
-
|
|
98
|
+
const Content = (
|
|
99
|
+
<SafeAreaView edges={["top", "bottom"]} style={styles.safeArea}>
|
|
100
|
+
<View style={styles.header}>
|
|
101
|
+
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
|
102
|
+
<AtomicText style={[styles.closeIcon, { color: tokens.colors.textSecondary }]}>
|
|
103
|
+
×
|
|
104
|
+
</AtomicText>
|
|
105
|
+
</TouchableOpacity>
|
|
106
|
+
<AtomicText
|
|
107
|
+
type="headlineMedium"
|
|
108
|
+
style={[styles.title, { color: tokens.colors.textPrimary }]}
|
|
109
|
+
>
|
|
110
|
+
{title}
|
|
111
|
+
</AtomicText>
|
|
112
|
+
{subtitle && (
|
|
113
|
+
<AtomicText
|
|
114
|
+
type="bodyMedium"
|
|
115
|
+
style={[styles.subtitle, { color: tokens.colors.textSecondary }]}
|
|
116
|
+
>
|
|
117
|
+
{subtitle}
|
|
118
|
+
</AtomicText>
|
|
119
|
+
)}
|
|
120
|
+
</View>
|
|
97
121
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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>
|
|
122
|
+
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}>
|
|
123
|
+
<SubscriptionPackageList
|
|
124
|
+
packages={packages}
|
|
125
|
+
isLoading={isLoading}
|
|
126
|
+
selectedPkg={selectedPkg}
|
|
127
|
+
onSelect={setSelectedPkg}
|
|
128
|
+
loadingText={loadingText}
|
|
129
|
+
emptyText={emptyText}
|
|
130
|
+
/>
|
|
131
|
+
{features.length > 0 && (
|
|
132
|
+
<View style={[styles.featuresSection, { backgroundColor: tokens.colors.surfaceSecondary }]}>
|
|
133
|
+
<PaywallFeaturesList features={features} gap={12} />
|
|
134
|
+
</View>
|
|
135
|
+
)}
|
|
136
|
+
</ScrollView>
|
|
125
137
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
)}
|
|
138
|
+
<SubscriptionFooter
|
|
139
|
+
isProcessing={isProcessing}
|
|
140
|
+
isLoading={isLoading}
|
|
141
|
+
processingText={processingText}
|
|
142
|
+
purchaseButtonText={purchaseButtonText}
|
|
143
|
+
hasPackages={packages.length > 0}
|
|
144
|
+
selectedPkg={selectedPkg}
|
|
145
|
+
restoreButtonText={restoreButtonText}
|
|
146
|
+
showRestoreButton={showRestoreButton}
|
|
147
|
+
privacyUrl={privacyUrl}
|
|
148
|
+
termsUrl={termsUrl}
|
|
149
|
+
privacyText={privacyText}
|
|
150
|
+
termsOfServiceText={termsOfServiceText}
|
|
151
|
+
onPurchase={handlePurchase}
|
|
152
|
+
onRestore={handleRestore}
|
|
153
|
+
/>
|
|
154
|
+
</SafeAreaView>
|
|
155
|
+
);
|
|
159
156
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
</View>
|
|
164
|
-
)}
|
|
165
|
-
</ScrollView>
|
|
157
|
+
if (variant === "fullscreen") {
|
|
158
|
+
const Wrapper = BackgroundComponent || View;
|
|
159
|
+
const wrapperStyle = !BackgroundComponent ? { flex: 1, backgroundColor: tokens.colors.backgroundPrimary } : { flex: 1 };
|
|
166
160
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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>
|
|
161
|
+
return (
|
|
162
|
+
<Modal visible={visible} transparent={false} animationType="slide" onRequestClose={onClose}>
|
|
163
|
+
<Wrapper style={wrapperStyle}>
|
|
164
|
+
{Content}
|
|
165
|
+
</Wrapper>
|
|
166
|
+
</Modal>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
187
169
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
</SafeAreaView>
|
|
170
|
+
return (
|
|
171
|
+
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
|
|
172
|
+
<View style={styles.overlay}>
|
|
173
|
+
<TouchableOpacity style={styles.backdrop} activeOpacity={1} onPress={onClose} />
|
|
174
|
+
<View style={[styles.bottomSheetContent, { backgroundColor: tokens.colors.surface }]}>
|
|
175
|
+
{Content}
|
|
195
176
|
</View>
|
|
196
177
|
</View>
|
|
197
178
|
</Modal>
|
|
@@ -204,20 +185,14 @@ SubscriptionModal.displayName = "SubscriptionModal";
|
|
|
204
185
|
const styles = StyleSheet.create({
|
|
205
186
|
overlay: { flex: 1, justifyContent: "flex-end" },
|
|
206
187
|
backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
|
207
|
-
|
|
208
|
-
safeArea: { paddingTop: 16 },
|
|
188
|
+
bottomSheetContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, maxHeight: "90%" },
|
|
189
|
+
safeArea: { flex: 1, paddingTop: 16 },
|
|
209
190
|
header: { alignItems: "center", paddingHorizontal: 24, paddingBottom: 20 },
|
|
210
191
|
closeButton: { position: "absolute", top: 0, right: 16, padding: 8, zIndex: 1 },
|
|
211
192
|
closeIcon: { fontSize: 28, fontWeight: "300" },
|
|
212
193
|
title: { marginBottom: 8, textAlign: "center" },
|
|
213
194
|
subtitle: { textAlign: "center" },
|
|
214
|
-
scrollView: {
|
|
195
|
+
scrollView: { flex: 1 },
|
|
215
196
|
scrollContent: { paddingHorizontal: 24, paddingBottom: 16 },
|
|
216
|
-
|
|
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 },
|
|
197
|
+
featuresSection: { borderRadius: 16, padding: 16, marginTop: 20 },
|
|
223
198
|
});
|
|
@@ -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
|
+
});
|