@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.7",
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
- * 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,84 @@ 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 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
- return (
99
- <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
100
- <View style={styles.overlay}>
101
- <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>
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
- <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
- )}
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
- {features.length > 0 && (
161
- <View style={[styles.featuresSection, { backgroundColor: tokens.colors.surfaceSecondary }]}>
162
- <PaywallFeaturesList features={features} gap={12} />
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
- <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>
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
- <PaywallLegalFooter
189
- privacyUrl={privacyUrl}
190
- termsUrl={termsUrl}
191
- privacyText={privacyText}
192
- termsOfServiceText={termsOfServiceText}
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
- content: { borderTopLeftRadius: 24, borderTopRightRadius: 24, maxHeight: "90%" },
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: { maxHeight: 400 },
195
+ scrollView: { flex: 1 },
215
196
  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 },
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
+ });