@umituz/react-native-subscription 2.14.82 → 2.14.84

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.14.82",
3
+ "version": "2.14.84",
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,190 +1,57 @@
1
1
  /**
2
2
  * PaywallContainer Component
3
- * Package-driven paywall with mode-based filtering
4
- * Mode: credits | subscription | hybrid
5
3
  */
6
4
 
7
- import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
8
- import type { PurchasesPackage } from "react-native-purchases";
5
+ import React, { useEffect, useMemo, useRef } from "react";
9
6
  import { usePaywallVisibility } from "../../../presentation/hooks/usePaywallVisibility";
10
7
  import { useSubscriptionPackages } from "../../../revenuecat/presentation/hooks/useSubscriptionPackages";
11
- import { usePurchasePackage } from "../../../revenuecat/presentation/hooks/usePurchasePackage";
12
- import { useRestorePurchase } from "../../../revenuecat/presentation/hooks/useRestorePurchase";
13
- import { SubscriptionManager } from "../../../revenuecat/infrastructure/managers/SubscriptionManager";
14
8
  import { filterPackagesByMode } from "../../../utils/packageFilter";
15
9
  import { createCreditAmountsFromPackages } from "../../../utils/creditMapper";
16
10
  import { PaywallModal } from "./PaywallModal";
11
+ import { usePaywallActions } from "../hooks/usePaywallActions";
17
12
  import type { PaywallContainerProps } from "./PaywallContainer.types";
18
13
 
19
- declare const __DEV__: boolean;
20
14
 
21
- export const PaywallContainer: React.FC<PaywallContainerProps> = ({
22
- userId,
23
- isAnonymous = false,
24
- translations,
25
- mode = "subscription",
26
- legalUrls,
27
- features,
28
- heroImage,
29
- bestValueIdentifier,
30
- creditsLabel,
31
- creditAmounts,
32
- packageFilterConfig,
33
- onPurchaseSuccess,
34
- onPurchaseError,
35
- onAuthRequired,
36
- visible,
37
- onClose,
38
- }) => {
39
- const { showPaywall: globalShowPaywall, closePaywall: globalClosePaywall } = usePaywallVisibility();
40
-
41
- const isVisible = visible !== undefined ? visible : globalShowPaywall;
42
- const handleClose = onClose !== undefined ? onClose : globalClosePaywall;
43
15
 
44
- const { data: allPackages = [], isLoading, isFetching, status, error } = useSubscriptionPackages(userId ?? undefined);
45
- const { mutateAsync: purchasePackage } = usePurchasePackage(userId ?? undefined);
46
- const { mutateAsync: restorePurchases } = useRestorePurchase(userId ?? undefined);
16
+ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
17
+ const { userId, isAnonymous = false, translations, mode = "subscription", legalUrls, features, heroImage, bestValueIdentifier, creditsLabel, creditAmounts, packageFilterConfig, onPurchaseSuccess, onPurchaseError, onAuthRequired, visible, onClose } = props;
47
18
 
48
- // Store pending package for post-auth purchase
49
- const [pendingPackage, setPendingPackage] = useState<PurchasesPackage | null>(null);
50
- const wasAnonymousRef = useRef(isAnonymous);
19
+ const { showPaywall, closePaywall } = usePaywallVisibility();
20
+ const isVisible = visible ?? showPaywall;
21
+ const handleClose = onClose ?? closePaywall;
51
22
 
52
- const { filteredPackages, computedCreditAmounts } = useMemo(() => {
53
- const filtered = filterPackagesByMode(allPackages, mode, packageFilterConfig);
54
- const computed = mode !== "subscription" && !creditAmounts
55
- ? createCreditAmountsFromPackages(allPackages)
56
- : creditAmounts;
57
-
58
- return { filteredPackages: filtered, computedCreditAmounts: computed };
59
- }, [allPackages, mode, packageFilterConfig, creditAmounts]);
23
+ const { data: allPackages = [], isLoading } = useSubscriptionPackages(userId ?? undefined);
24
+ const { handlePurchase, handleRestore, pendingPackage, setPendingPackage, purchasePackage } = usePaywallActions({
25
+ userId: userId ?? undefined, isAnonymous, onPurchaseSuccess, onPurchaseError, onAuthRequired, onClose: handleClose
26
+ });
60
27
 
61
- useEffect(() => {
62
- if (__DEV__ && isVisible) {
63
- console.log("[PaywallContainer] Paywall opened:", {
64
- userId,
65
- isAnonymous,
66
- mode,
67
- isConfigured: SubscriptionManager.isConfigured(),
68
- isInitialized: SubscriptionManager.isInitialized(),
69
- allPackagesCount: allPackages.length,
70
- filteredPackagesCount: filteredPackages.length,
71
- computedCreditAmounts,
72
- isLoading,
73
- isFetching,
74
- status,
75
- error: error?.message ?? null,
76
- });
77
- }
78
- }, [isVisible, userId, isAnonymous, mode, allPackages.length, filteredPackages.length, isLoading, isFetching, status, error]);
28
+ const wasAnonymousRef = useRef(isAnonymous);
29
+ const { filteredPackages, computedCreditAmounts } = useMemo(() => ({
30
+ filteredPackages: filterPackagesByMode(allPackages, mode, packageFilterConfig),
31
+ computedCreditAmounts: mode !== "subscription" && !creditAmounts ? createCreditAmountsFromPackages(allPackages) : creditAmounts
32
+ }), [allPackages, mode, packageFilterConfig, creditAmounts]);
79
33
 
80
- // Auto-purchase when user authenticates after selecting a package
34
+ // Auto-purchase after auth
81
35
  useEffect(() => {
82
- const wasAnonymous = wasAnonymousRef.current;
36
+ const wasAuth = wasAnonymousRef.current && !isAnonymous;
83
37
  wasAnonymousRef.current = isAnonymous;
84
38
 
85
- if (__DEV__) {
86
- console.log("[PaywallContainer] Auth state check:", {
87
- wasAnonymous,
88
- isAnonymous,
89
- hasPendingPackage: !!pendingPackage,
90
- userId,
91
- pendingPkgId: pendingPackage?.identifier,
92
- });
93
- }
94
-
95
- // If user was anonymous, now authenticated, and has pending package
96
- if (wasAnonymous && !isAnonymous && pendingPackage && userId) {
97
- if (__DEV__) {
98
- console.log("[PaywallContainer] User authenticated, auto-purchasing pending package:", pendingPackage.identifier);
99
- }
100
-
101
- // Execute the purchase
39
+ if (wasAuth && pendingPackage && userId) {
102
40
  (async () => {
103
41
  try {
104
- const result = await purchasePackage(pendingPackage);
105
- if (result.success) {
106
- if (__DEV__) {
107
- console.log("[PaywallContainer] Auto-purchase successful");
108
- }
42
+ const res = await purchasePackage(pendingPackage);
43
+ if (res.success) {
109
44
  onPurchaseSuccess?.();
110
45
  handleClose();
111
46
  }
112
- } catch (err) {
113
- const message = err instanceof Error ? err.message : String(err);
114
- if (__DEV__) {
115
- console.error("[PaywallContainer] Auto-purchase failed:", message);
116
- }
117
- onPurchaseError?.(message);
118
- } finally {
119
- setPendingPackage(null);
120
- }
47
+ } catch (err: any) {
48
+ onPurchaseError?.(err.message || String(err));
49
+ } finally { setPendingPackage(null); }
121
50
  })();
122
51
  }
123
- }, [isAnonymous, userId, pendingPackage, purchasePackage, onPurchaseSuccess, onPurchaseError, handleClose]);
124
-
125
- const handlePurchase = useCallback(
126
- async (pkg: PurchasesPackage) => {
127
- // Auth gating: require authentication for anonymous users
128
- if (isAnonymous) {
129
- if (__DEV__) {
130
- console.log("[PaywallContainer] Anonymous user, storing package and requiring auth:", pkg.identifier);
131
- console.log("[PaywallContainer] onAuthRequired is defined:", !!onAuthRequired);
132
- }
133
- // Store package for auto-purchase after auth
134
- setPendingPackage(pkg);
135
- // Don't close paywall - keep it open so user can purchase after auth
136
- onAuthRequired?.();
137
- return;
138
- }
139
-
140
- try {
141
- if (__DEV__) {
142
- console.log("[PaywallContainer] Purchase started:", pkg.identifier);
143
- }
144
- const result = await purchasePackage(pkg);
145
- if (result.success) {
146
- if (__DEV__) {
147
- console.log("[PaywallContainer] Purchase successful");
148
- }
149
- onPurchaseSuccess?.();
150
- handleClose();
151
- }
152
- } catch (err) {
153
- const message = err instanceof Error ? err.message : String(err);
154
- if (__DEV__) {
155
- console.error("[PaywallContainer] Purchase failed:", message);
156
- }
157
- onPurchaseError?.(message);
158
- }
159
- },
160
- [isAnonymous, purchasePackage, handleClose, onPurchaseSuccess, onPurchaseError, onAuthRequired, setPendingPackage]
161
- );
162
-
163
- const handleRestore = useCallback(async () => {
164
- try {
165
- if (__DEV__) {
166
- console.log("[PaywallContainer] Restore started");
167
- }
168
- const result = await restorePurchases();
169
- if (result.success) {
170
- if (__DEV__) {
171
- console.log("[PaywallContainer] Restore successful");
172
- }
173
- onPurchaseSuccess?.();
174
- handleClose();
175
- }
176
- } catch (err) {
177
- const message = err instanceof Error ? err.message : String(err);
178
- if (__DEV__) {
179
- console.error("[PaywallContainer] Restore failed:", message);
180
- }
181
- onPurchaseError?.(message);
182
- }
183
- }, [restorePurchases, handleClose, onPurchaseSuccess, onPurchaseError]);
52
+ }, [isAnonymous, userId, pendingPackage, purchasePackage, onPurchaseSuccess, onPurchaseError, handleClose, setPendingPackage]);
184
53
 
185
- if (!isVisible) {
186
- return null;
187
- }
54
+ if (!isVisible) return null;
188
55
 
189
56
  return (
190
57
  <PaywallModal
@@ -0,0 +1,25 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+ import { AtomicText, AtomicIcon, useAppDesignTokens } from "@umituz/react-native-design-system";
4
+ import type { SubscriptionFeature } from "../entities";
5
+ import { paywallModalStyles as styles } from "./PaywallModal.styles";
6
+
7
+ export const PaywallFeatures: React.FC<{ features: SubscriptionFeature[] }> = ({ features }) => {
8
+ const tokens = useAppDesignTokens();
9
+ if (!features.length) return null;
10
+
11
+ return (
12
+ <View style={[styles.features, { backgroundColor: tokens.colors.surfaceSecondary }]}>
13
+ {features.map((feature, idx) => (
14
+ <View key={idx} style={styles.featureRow}>
15
+ <View style={[styles.featureIcon, { backgroundColor: tokens.colors.primaryLight }]}>
16
+ <AtomicIcon name={feature.icon} customSize={16} customColor={tokens.colors.primary} />
17
+ </View>
18
+ <AtomicText type="bodyMedium" style={[styles.featureText, { color: tokens.colors.textPrimary }]}>
19
+ {feature.text}
20
+ </AtomicText>
21
+ </View>
22
+ ))}
23
+ </View>
24
+ );
25
+ };
@@ -1,98 +1,51 @@
1
- /**
2
- * Paywall Footer
3
- * Action button and legal links
4
- */
5
-
6
1
  import React from "react";
7
- import { View, TouchableOpacity, StyleSheet, Linking } from "react-native";
8
- import { AtomicText, AtomicButton, useAppDesignTokens } from "@umituz/react-native-design-system";
2
+ import { View, TouchableOpacity } from "react-native";
3
+ import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
4
+ import type { PaywallTranslations, PaywallLegalUrls } from "../entities";
5
+ import { paywallModalStyles as styles } from "./PaywallModal.styles";
9
6
 
10
7
  interface PaywallFooterProps {
11
- isProcessing: boolean;
12
- isDisabled: boolean;
13
- purchaseButtonText: string;
14
- processingText: string;
15
- restoreButtonText: string;
16
- privacyText?: string;
17
- termsText?: string;
18
- privacyUrl?: string;
19
- termsUrl?: string;
20
- onPurchase: () => void;
21
- onRestore: () => void;
8
+ translations: PaywallTranslations;
9
+ legalUrls: PaywallLegalUrls;
10
+ isProcessing: boolean;
11
+ onRestore?: () => Promise<void | boolean>;
12
+ onLegalClick: (url: string | undefined) => void;
22
13
  }
23
14
 
24
- export const PaywallFooter: React.FC<PaywallFooterProps> = React.memo(
25
- ({
26
- isProcessing,
27
- isDisabled,
28
- purchaseButtonText,
29
- processingText,
30
- restoreButtonText,
31
- privacyText,
32
- termsText,
33
- privacyUrl,
34
- termsUrl,
35
- onPurchase,
36
- onRestore,
37
- }) => {
38
- const tokens = useAppDesignTokens();
39
-
40
- const handleOpenUrl = (url?: string) => {
41
- if (url) Linking.openURL(url);
42
- };
43
-
44
- return (
45
- <View style={styles.container}>
46
- <AtomicButton
47
- title={isProcessing ? processingText : purchaseButtonText}
48
- onPress={onPurchase}
49
- disabled={isDisabled || isProcessing}
50
- variant="primary"
51
- size="lg"
52
- style={styles.purchaseButton}
53
- />
54
-
55
- <View style={styles.linksRow}>
56
- {termsText && termsUrl && (
57
- <TouchableOpacity onPress={() => handleOpenUrl(termsUrl)}>
58
- <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
59
- {termsText}
60
- </AtomicText>
61
- </TouchableOpacity>
62
- )}
63
-
64
- <TouchableOpacity onPress={onRestore}>
65
- <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
66
- {restoreButtonText}
67
- </AtomicText>
68
- </TouchableOpacity>
69
-
70
- {privacyText && privacyUrl && (
71
- <TouchableOpacity onPress={() => handleOpenUrl(privacyUrl)}>
72
- <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
73
- {privacyText}
74
- </AtomicText>
75
- </TouchableOpacity>
76
- )}
77
- </View>
78
- </View>
79
- );
80
- }
81
- );
82
-
83
- PaywallFooter.displayName = "PaywallFooter";
84
-
85
- const styles = StyleSheet.create({
86
- container: {
87
- paddingHorizontal: 24,
88
- paddingBottom: 32,
89
- },
90
- purchaseButton: {
91
- marginBottom: 16,
92
- },
93
- linksRow: {
94
- flexDirection: "row",
95
- justifyContent: "space-between",
96
- paddingHorizontal: 8,
97
- },
98
- });
15
+ export const PaywallFooter: React.FC<PaywallFooterProps> = ({
16
+ translations,
17
+ legalUrls,
18
+ isProcessing,
19
+ onRestore,
20
+ onLegalClick,
21
+ }) => {
22
+ const tokens = useAppDesignTokens();
23
+
24
+ return (
25
+ <View style={styles.footer}>
26
+ {onRestore && (
27
+ <TouchableOpacity onPress={onRestore} disabled={isProcessing} style={[styles.restoreButton, isProcessing && styles.restoreButtonDisabled]}>
28
+ <AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.textSecondary }]}>
29
+ {isProcessing ? translations.processingText : translations.restoreButtonText}
30
+ </AtomicText>
31
+ </TouchableOpacity>
32
+ )}
33
+ <View style={styles.legalRow}>
34
+ {legalUrls.termsUrl && (
35
+ <TouchableOpacity onPress={() => onLegalClick(legalUrls.termsUrl)}>
36
+ <AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.textSecondary }]}>
37
+ {translations.termsOfServiceText}
38
+ </AtomicText>
39
+ </TouchableOpacity>
40
+ )}
41
+ {legalUrls.privacyUrl && (
42
+ <TouchableOpacity onPress={() => onLegalClick(legalUrls.privacyUrl)}>
43
+ <AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.textSecondary }]}>
44
+ {translations.privacyText}
45
+ </AtomicText>
46
+ </TouchableOpacity>
47
+ )}
48
+ </View>
49
+ </View>
50
+ );
51
+ };
@@ -1,7 +1,5 @@
1
1
  /**
2
2
  * Paywall Modal
3
- * Renders packages passed from PaywallContainer
4
- * Filtering is handled by PaywallContainer based on mode
5
3
  */
6
4
 
7
5
  import React, { useState, useCallback } from "react";
@@ -12,6 +10,8 @@ import type { PurchasesPackage } from "react-native-purchases";
12
10
  import { PlanCard } from "./PlanCard";
13
11
  import type { SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from "../entities";
14
12
  import { paywallModalStyles as styles } from "./PaywallModal.styles";
13
+ import { PaywallFeatures } from "./PaywallFeatures";
14
+ import { PaywallFooter } from "./PaywallFooter";
15
15
 
16
16
  export interface PaywallModalProps {
17
17
  visible: boolean;
@@ -30,22 +30,7 @@ export interface PaywallModalProps {
30
30
  }
31
31
 
32
32
  export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
33
- const {
34
- visible,
35
- onClose,
36
- translations,
37
- packages = [],
38
- features = [],
39
- isLoading = false,
40
- legalUrls = {},
41
- bestValueIdentifier,
42
- creditAmounts,
43
- creditsLabel,
44
- heroImage,
45
- onPurchase,
46
- onRestore,
47
- } = props;
48
-
33
+ const { visible, onClose, translations, packages = [], features = [], isLoading = false, legalUrls = {}, bestValueIdentifier, creditAmounts, creditsLabel, heroImage, onPurchase, onRestore } = props;
49
34
  const tokens = useAppDesignTokens();
50
35
  const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
51
36
  const [isProcessing, setIsProcessing] = useState(false);
@@ -56,41 +41,24 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
56
41
  try {
57
42
  const pkg = packages.find((p) => p.product.identifier === selectedPlanId);
58
43
  if (pkg) await onPurchase(pkg);
59
- } finally {
60
- setIsProcessing(false);
61
- }
44
+ } finally { setIsProcessing(false); }
62
45
  }, [selectedPlanId, packages, onPurchase]);
63
46
 
64
47
  const handleRestore = useCallback(async () => {
65
48
  if (!onRestore || isProcessing) return;
66
49
  setIsProcessing(true);
67
- try {
68
- await onRestore();
69
- } finally {
70
- setIsProcessing(false);
71
- }
50
+ try { await onRestore(); } finally { setIsProcessing(false); }
72
51
  }, [onRestore, isProcessing]);
73
52
 
74
53
  const handleLegalUrl = useCallback(async (url: string | undefined) => {
75
54
  if (!url) return;
76
- try {
77
- const supported = await Linking.canOpenURL(url);
78
- if (supported) await Linking.openURL(url);
79
- } catch {
80
- // Silent fail
81
- }
55
+ try { if (await Linking.canOpenURL(url)) await Linking.openURL(url); } catch { /* Silent fail */ }
82
56
  }, []);
83
57
 
84
- const isPurchaseDisabled = !selectedPlanId;
85
-
86
58
  return (
87
59
  <BaseModal visible={visible} onClose={onClose} contentStyle={styles.modalContent}>
88
60
  <View style={[styles.container, { backgroundColor: tokens.colors.surface }]}>
89
- <TouchableOpacity
90
- onPress={onClose}
91
- style={[styles.closeBtn, { backgroundColor: tokens.colors.surfaceSecondary }]}
92
- hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
93
- >
61
+ <TouchableOpacity onPress={onClose} style={[styles.closeBtn, { backgroundColor: tokens.colors.surfaceSecondary }]} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
94
62
  <AtomicIcon name="close-outline" size="md" customColor={tokens.colors.textPrimary} />
95
63
  </TouchableOpacity>
96
64
 
@@ -102,35 +70,14 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
102
70
  )}
103
71
 
104
72
  <View style={styles.header}>
105
- <AtomicText type="headlineMedium" style={[styles.title, { color: tokens.colors.textPrimary }]}>
106
- {translations.title}
107
- </AtomicText>
108
- {translations.subtitle && (
109
- <AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>
110
- {translations.subtitle}
111
- </AtomicText>
112
- )}
73
+ <AtomicText type="headlineMedium" style={[styles.title, { color: tokens.colors.textPrimary }]}>{translations.title}</AtomicText>
74
+ {translations.subtitle && <AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>{translations.subtitle}</AtomicText>}
113
75
  </View>
114
76
 
115
- {features.length > 0 && (
116
- <View style={[styles.features, { backgroundColor: tokens.colors.surfaceSecondary }]}>
117
- {features.map((feature, idx) => (
118
- <View key={idx} style={styles.featureRow}>
119
- <View style={[styles.featureIcon, { backgroundColor: tokens.colors.primaryLight }]}>
120
- <AtomicIcon name={feature.icon} customSize={16} customColor={tokens.colors.primary} />
121
- </View>
122
- <AtomicText type="bodyMedium" style={[styles.featureText, { color: tokens.colors.textPrimary }]}>
123
- {feature.text}
124
- </AtomicText>
125
- </View>
126
- ))}
127
- </View>
128
- )}
77
+ <PaywallFeatures features={features} />
129
78
 
130
79
  {isLoading ? (
131
- <View style={styles.loading}>
132
- <AtomicSpinner size="lg" color="primary" text={translations.loadingText} />
133
- </View>
80
+ <View style={styles.loading}><AtomicSpinner size="lg" color="primary" text={translations.loadingText} /></View>
134
81
  ) : (
135
82
  <View style={styles.plans}>
136
83
  {packages.map((pkg) => (
@@ -149,12 +96,8 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
149
96
 
150
97
  <TouchableOpacity
151
98
  onPress={handlePurchase}
152
- disabled={isPurchaseDisabled || isProcessing}
153
- style={[
154
- styles.cta,
155
- { backgroundColor: tokens.colors.primary },
156
- (isPurchaseDisabled || isProcessing) && styles.ctaDisabled,
157
- ]}
99
+ disabled={!selectedPlanId || isProcessing}
100
+ style={[styles.cta, { backgroundColor: tokens.colors.primary }, (!selectedPlanId || isProcessing) && styles.ctaDisabled]}
158
101
  activeOpacity={0.8}
159
102
  >
160
103
  <AtomicText type="titleLarge" style={[styles.ctaText, { color: tokens.colors.onPrimary }]}>
@@ -162,35 +105,7 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
162
105
  </AtomicText>
163
106
  </TouchableOpacity>
164
107
 
165
- <View style={styles.footer}>
166
- {onRestore && (
167
- <TouchableOpacity
168
- onPress={handleRestore}
169
- disabled={isProcessing}
170
- style={[styles.restoreButton, isProcessing && styles.restoreButtonDisabled]}
171
- >
172
- <AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.textSecondary }]}>
173
- {isProcessing ? translations.processingText : translations.restoreButtonText}
174
- </AtomicText>
175
- </TouchableOpacity>
176
- )}
177
- <View style={styles.legalRow}>
178
- {legalUrls.termsUrl && (
179
- <TouchableOpacity onPress={() => handleLegalUrl(legalUrls.termsUrl)}>
180
- <AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.textSecondary }]}>
181
- {translations.termsOfServiceText}
182
- </AtomicText>
183
- </TouchableOpacity>
184
- )}
185
- {legalUrls.privacyUrl && (
186
- <TouchableOpacity onPress={() => handleLegalUrl(legalUrls.privacyUrl)}>
187
- <AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.textSecondary }]}>
188
- {translations.privacyText}
189
- </AtomicText>
190
- </TouchableOpacity>
191
- )}
192
- </View>
193
- </View>
108
+ <PaywallFooter translations={translations} legalUrls={legalUrls} isProcessing={isProcessing} onRestore={onRestore ? handleRestore : undefined} onLegalClick={handleLegalUrl} />
194
109
  </ScrollView>
195
110
  </View>
196
111
  </BaseModal>
@@ -0,0 +1,63 @@
1
+ import { useCallback, useState } from "react";
2
+ import type { PurchasesPackage } from "react-native-purchases";
3
+ import { usePurchasePackage } from "../../../revenuecat/presentation/hooks/usePurchasePackage";
4
+ import { useRestorePurchase } from "../../../revenuecat/presentation/hooks/useRestorePurchase";
5
+
6
+ declare const __DEV__: boolean;
7
+
8
+ interface UsePaywallActionsProps {
9
+ userId?: string;
10
+ isAnonymous: boolean;
11
+ onPurchaseSuccess?: () => void;
12
+ onPurchaseError?: (error: string) => void;
13
+ onAuthRequired?: () => void;
14
+ onClose: () => void;
15
+ }
16
+
17
+ export const usePaywallActions = ({
18
+ userId,
19
+ isAnonymous,
20
+ onPurchaseSuccess,
21
+ onPurchaseError,
22
+ onAuthRequired,
23
+ onClose,
24
+ }: UsePaywallActionsProps) => {
25
+ const { mutateAsync: purchasePackage } = usePurchasePackage(userId);
26
+ const { mutateAsync: restorePurchases } = useRestorePurchase(userId);
27
+ const [pendingPackage, setPendingPackage] = useState<PurchasesPackage | null>(null);
28
+
29
+ const handlePurchase = useCallback(async (pkg: PurchasesPackage) => {
30
+ if (isAnonymous) {
31
+ if (__DEV__) console.log("[PaywallActions] Anonymous user, storing package:", pkg.identifier);
32
+ setPendingPackage(pkg);
33
+ onAuthRequired?.();
34
+ return;
35
+ }
36
+
37
+ try {
38
+ if (__DEV__) console.log("[PaywallActions] Purchase started:", pkg.identifier);
39
+ const res = await purchasePackage(pkg);
40
+ if (res.success) {
41
+ onPurchaseSuccess?.();
42
+ onClose();
43
+ }
44
+ } catch (err: any) {
45
+ onPurchaseError?.(err.message || String(err));
46
+ }
47
+ }, [isAnonymous, purchasePackage, onClose, onPurchaseSuccess, onPurchaseError, onAuthRequired]);
48
+
49
+ const handleRestore = useCallback(async () => {
50
+ try {
51
+ if (__DEV__) console.log("[PaywallActions] Restore started");
52
+ const res = await restorePurchases();
53
+ if (res.success) {
54
+ onPurchaseSuccess?.();
55
+ onClose();
56
+ }
57
+ } catch (err: any) {
58
+ onPurchaseError?.(err.message || String(err));
59
+ }
60
+ }, [restorePurchases, onClose, onPurchaseSuccess, onPurchaseError]);
61
+
62
+ return { handlePurchase, handleRestore, pendingPackage, setPendingPackage, purchasePackage };
63
+ };