@umituz/react-native-subscription 2.12.6 → 2.12.7

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.12.6",
3
+ "version": "2.12.7",
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",
@@ -1,26 +1,22 @@
1
1
  /**
2
2
  * Paywall Modal
3
- * Mode-based paywall: subscription, credits, or hybrid
3
+ * Modern paywall with gradient container and responsive design
4
4
  */
5
5
 
6
6
  import React, { useState, useCallback } from "react";
7
- import { View, ScrollView, StyleSheet, ActivityIndicator } from "react-native";
8
- import { BaseModal, useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
7
+ import { View, ScrollView, StyleSheet, TouchableOpacity, ActivityIndicator } from "react-native";
8
+ import { LinearGradient } from "expo-linear-gradient";
9
+ import {
10
+ BaseModal,
11
+ useAppDesignTokens,
12
+ AtomicText,
13
+ AtomicIcon,
14
+ useDesignSystemTheme,
15
+ } from "@umituz/react-native-design-system";
9
16
  import type { PurchasesPackage } from "react-native-purchases";
10
- import { PaywallHeader } from "./PaywallHeader";
11
- import { PaywallTabBar } from "./PaywallTabBar";
12
- import { PaywallFooter } from "./PaywallFooter";
13
- import { FeatureList } from "./FeatureList";
14
17
  import { PlanCard } from "./PlanCard";
15
18
  import { CreditCard } from "./CreditCard";
16
- import type {
17
- PaywallMode,
18
- PaywallTabType,
19
- CreditsPackage,
20
- SubscriptionFeature,
21
- PaywallTranslations,
22
- PaywallLegalUrls,
23
- } from "../entities";
19
+ import type { PaywallMode, CreditsPackage, SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from "../entities";
24
20
 
25
21
  export interface PaywallModalProps {
26
22
  visible: boolean;
@@ -60,15 +56,19 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
60
56
  } = props;
61
57
 
62
58
  const tokens = useAppDesignTokens();
63
- const initialTab: PaywallTabType = mode === "credits" ? "credits" : "subscription";
64
- const [activeTab, setActiveTab] = useState<PaywallTabType>(initialTab);
59
+ const { themeMode } = useDesignSystemTheme();
60
+ const isDark = themeMode === "dark";
61
+
65
62
  const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
66
63
  const [selectedCreditId, setSelectedCreditId] = useState<string | null>(null);
67
64
  const [isProcessing, setIsProcessing] = useState(false);
68
65
 
69
- const showTabs = mode === "hybrid";
70
- const showCredits = mode === "credits" || (mode === "hybrid" && activeTab === "credits");
71
- const showSubscription = mode === "subscription" || (mode === "hybrid" && activeTab === "subscription");
66
+ const showCredits = mode === "credits";
67
+ const showSubscription = mode === "subscription" || mode === "hybrid";
68
+
69
+ const gradientColors: readonly [string, string, string] = isDark
70
+ ? ["#1e3a5f", "#2d1e3f", "#1a1a2e"]
71
+ : ["#4a5568", "#5a4a78", "#3a3a4a"];
72
72
 
73
73
  const handlePurchase = useCallback(async () => {
74
74
  setIsProcessing(true);
@@ -85,44 +85,64 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
85
85
  }, [showSubscription, showCredits, selectedPlanId, selectedCreditId, subscriptionPackages, onSubscriptionPurchase, onCreditsPurchase]);
86
86
 
87
87
  const handleRestore = useCallback(async () => {
88
- if (onRestore) {
89
- setIsProcessing(true);
90
- try {
91
- await onRestore();
92
- } finally {
93
- setIsProcessing(false);
94
- }
88
+ if (!onRestore || isProcessing) return;
89
+ setIsProcessing(true);
90
+ try {
91
+ await onRestore();
92
+ } finally {
93
+ setIsProcessing(false);
95
94
  }
96
- }, [onRestore]);
95
+ }, [onRestore, isProcessing]);
97
96
 
98
97
  const isPurchaseDisabled = showSubscription ? !selectedPlanId : !selectedCreditId;
99
98
 
100
99
  return (
101
- <BaseModal visible={visible} onClose={onClose}>
102
- <View style={styles.container}>
103
- <PaywallHeader title={translations.title} subtitle={translations.subtitle} onClose={onClose} />
104
-
105
- {showTabs && (
106
- <PaywallTabBar
107
- activeTab={activeTab}
108
- onTabChange={setActiveTab}
109
- creditsLabel={translations.creditsTabLabel ?? "Credits"}
110
- subscriptionLabel={translations.subscriptionTabLabel ?? "Subscription"}
111
- />
112
- )}
113
-
114
- <ScrollView style={styles.scroll} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
115
- <FeatureList features={features} />
100
+ <BaseModal visible={visible} onClose={onClose} contentStyle={styles.modalContent}>
101
+ <LinearGradient colors={gradientColors} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} style={styles.gradient}>
102
+ <TouchableOpacity
103
+ onPress={onClose}
104
+ style={[styles.closeBtn, { backgroundColor: tokens.colors.surface }]}
105
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
106
+ >
107
+ <AtomicIcon name="close-outline" size="md" customColor={tokens.colors.textPrimary} />
108
+ </TouchableOpacity>
109
+
110
+ <ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.scroll}>
111
+ <View style={styles.header}>
112
+ <AtomicText type="headlineLarge" style={[styles.title, { color: tokens.colors.onPrimary }]}>
113
+ {translations.title}
114
+ </AtomicText>
115
+ {translations.subtitle && (
116
+ <AtomicText type="bodyLarge" style={[styles.subtitle, { color: tokens.colors.onPrimary }]}>
117
+ {translations.subtitle}
118
+ </AtomicText>
119
+ )}
120
+ </View>
121
+
122
+ {features.length > 0 && (
123
+ <View style={[styles.features, { backgroundColor: `${tokens.colors.surface}15` }]}>
124
+ {features.map((feature, idx) => (
125
+ <View key={idx} style={styles.featureRow}>
126
+ <View style={[styles.featureIcon, { backgroundColor: tokens.colors.primaryLight }]}>
127
+ <AtomicIcon name={feature.icon} customSize={20} customColor={tokens.colors.primary} />
128
+ </View>
129
+ <AtomicText type="bodyLarge" style={[styles.featureText, { color: tokens.colors.onPrimary }]}>
130
+ {feature.text}
131
+ </AtomicText>
132
+ </View>
133
+ ))}
134
+ </View>
135
+ )}
116
136
 
117
137
  {isLoading ? (
118
- <View style={styles.loadingContainer}>
119
- <ActivityIndicator color={tokens.colors.primary} />
120
- <AtomicText type="bodyMedium" style={{ color: tokens.colors.textSecondary, marginTop: 12 }}>
138
+ <View style={styles.loading}>
139
+ <ActivityIndicator color={tokens.colors.onPrimary} />
140
+ <AtomicText type="bodyMedium" style={[styles.loadingText, { color: tokens.colors.onPrimary }]}>
121
141
  {translations.loadingText}
122
142
  </AtomicText>
123
143
  </View>
124
144
  ) : (
125
- <>
145
+ <View style={styles.plans}>
126
146
  {showSubscription &&
127
147
  subscriptionPackages.map((pkg) => (
128
148
  <PlanCard
@@ -135,34 +155,49 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
135
155
  creditsLabel={creditsLabel}
136
156
  />
137
157
  ))}
138
-
139
158
  {showCredits &&
140
159
  creditsPackages.map((pkg) => (
141
- <CreditCard
142
- key={pkg.id}
143
- pkg={pkg}
144
- isSelected={selectedCreditId === pkg.id}
145
- onSelect={() => setSelectedCreditId(pkg.id)}
146
- />
160
+ <CreditCard key={pkg.id} pkg={pkg} isSelected={selectedCreditId === pkg.id} onSelect={() => setSelectedCreditId(pkg.id)} />
147
161
  ))}
148
- </>
162
+ </View>
149
163
  )}
150
- </ScrollView>
151
164
 
152
- <PaywallFooter
153
- isProcessing={isProcessing}
154
- isDisabled={isPurchaseDisabled}
155
- purchaseButtonText={showSubscription ? (translations.subscribeButtonText ?? translations.purchaseButtonText) : translations.purchaseButtonText}
156
- processingText={translations.processingText}
157
- restoreButtonText={translations.restoreButtonText}
158
- privacyText={translations.privacyText}
159
- termsText={translations.termsOfServiceText}
160
- privacyUrl={legalUrls.privacyUrl}
161
- termsUrl={legalUrls.termsUrl}
162
- onPurchase={handlePurchase}
163
- onRestore={handleRestore}
164
- />
165
- </View>
165
+ <TouchableOpacity
166
+ onPress={handlePurchase}
167
+ disabled={isPurchaseDisabled || isProcessing}
168
+ style={[styles.cta, { backgroundColor: tokens.colors.primary }, (isPurchaseDisabled || isProcessing) && styles.ctaDisabled]}
169
+ activeOpacity={0.8}
170
+ >
171
+ <AtomicText type="titleLarge" style={[styles.ctaText, { color: tokens.colors.onPrimary }]}>
172
+ {isProcessing ? translations.processingText : showSubscription ? translations.subscribeButtonText : translations.purchaseButtonText}
173
+ </AtomicText>
174
+ </TouchableOpacity>
175
+
176
+ <View style={styles.footer}>
177
+ {legalUrls.termsUrl && (
178
+ <TouchableOpacity onPress={() => {}}>
179
+ <AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.onPrimary }]}>
180
+ {translations.termsOfServiceText}
181
+ </AtomicText>
182
+ </TouchableOpacity>
183
+ )}
184
+ {onRestore && (
185
+ <TouchableOpacity onPress={handleRestore}>
186
+ <AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.onPrimary }]}>
187
+ {translations.restoreButtonText}
188
+ </AtomicText>
189
+ </TouchableOpacity>
190
+ )}
191
+ {legalUrls.privacyUrl && (
192
+ <TouchableOpacity onPress={() => {}}>
193
+ <AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.onPrimary }]}>
194
+ {translations.privacyText}
195
+ </AtomicText>
196
+ </TouchableOpacity>
197
+ )}
198
+ </View>
199
+ </ScrollView>
200
+ </LinearGradient>
166
201
  </BaseModal>
167
202
  );
168
203
  });
@@ -170,18 +205,23 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
170
205
  PaywallModal.displayName = "PaywallModal";
171
206
 
172
207
  const styles = StyleSheet.create({
173
- container: {
174
- flex: 1,
175
- width: "100%",
176
- },
177
- scroll: {
178
- flex: 1,
179
- },
180
- scrollContent: {
181
- paddingBottom: 20,
182
- },
183
- loadingContainer: {
184
- alignItems: "center",
185
- paddingVertical: 40,
186
- },
208
+ modalContent: { padding: 0, borderWidth: 0, overflow: "hidden" },
209
+ gradient: { flex: 1 },
210
+ closeBtn: { position: "absolute", top: 16, right: 16, width: 36, height: 36, borderRadius: 18, justifyContent: "center", alignItems: "center", zIndex: 10 },
211
+ scroll: { padding: 24, paddingTop: 56 },
212
+ header: { alignItems: "center", marginBottom: 24 },
213
+ title: { fontWeight: "700", textAlign: "center", marginBottom: 8 },
214
+ subtitle: { textAlign: "center", lineHeight: 24, opacity: 0.8 },
215
+ features: { borderRadius: 16, padding: 16, marginBottom: 20, gap: 12 },
216
+ featureRow: { flexDirection: "row", alignItems: "center" },
217
+ featureIcon: { width: 40, height: 40, borderRadius: 20, justifyContent: "center", alignItems: "center", marginRight: 12 },
218
+ featureText: { flex: 1, fontWeight: "500" },
219
+ loading: { alignItems: "center", paddingVertical: 40 },
220
+ loadingText: { marginTop: 12, opacity: 0.7 },
221
+ plans: { marginBottom: 20 },
222
+ cta: { borderRadius: 16, paddingVertical: 18, alignItems: "center", marginBottom: 16 },
223
+ ctaDisabled: { opacity: 0.5 },
224
+ ctaText: { fontWeight: "700" },
225
+ footer: { flexDirection: "row", justifyContent: "space-between", paddingHorizontal: 8 },
226
+ footerLink: { opacity: 0.6 },
187
227
  });