@umituz/react-native-subscription 2.20.7 → 2.21.0

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.20.7",
3
+ "version": "2.21.0",
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",
@@ -13,7 +13,7 @@ import { usePaywallActions } from "../hooks/usePaywallActions";
13
13
  import type { PaywallContainerProps } from "./PaywallContainer.types";
14
14
 
15
15
  export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
16
- const { userId, isAnonymous: _isAnonymous = false, translations, mode = "subscription", legalUrls, features, heroImage, bestValueIdentifier, creditsLabel, creditAmounts, packageFilterConfig, source, onPurchaseSuccess, onPurchaseError, onAuthRequired, visible, onClose } = props;
16
+ const { userId, translations, mode = "subscription", legalUrls, features, heroImage, bestValueIdentifier, creditsLabel, creditAmounts, packageFilterConfig, source, onPurchaseSuccess, onPurchaseError, onAuthRequired, visible, onClose } = props;
17
17
 
18
18
  const { showPaywall, closePaywall, currentSource } = usePaywallVisibility();
19
19
  const isVisible = visible ?? showPaywall;
@@ -12,6 +12,9 @@ import type { SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from
12
12
  import { paywallModalStyles as styles } from "./PaywallModal.styles";
13
13
  import { PaywallFeatures } from "./PaywallFeatures";
14
14
  import { PaywallFooter } from "./PaywallFooter";
15
+ import { usePurchaseLoadingStore, selectIsPurchasing } from "../../../presentation/stores";
16
+
17
+ declare const __DEV__: boolean;
15
18
 
16
19
  export interface PaywallModalProps {
17
20
  visible: boolean;
@@ -33,21 +36,61 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
33
36
  const { visible, onClose, translations, packages = [], features = [], isLoading = false, legalUrls = {}, bestValueIdentifier, creditAmounts, creditsLabel, heroImage, onPurchase, onRestore } = props;
34
37
  const tokens = useAppDesignTokens();
35
38
  const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
36
- const [isProcessing, setIsProcessing] = useState(false);
39
+ const [isLocalProcessing, setIsLocalProcessing] = useState(false);
40
+
41
+ // Global purchase loading state (for auto-execution after auth)
42
+ const isGlobalPurchasing = usePurchaseLoadingStore(selectIsPurchasing);
43
+ const { startPurchase, endPurchase } = usePurchaseLoadingStore();
44
+
45
+ // Combined processing state
46
+ const isProcessing = isLocalProcessing || isGlobalPurchasing;
37
47
 
38
48
  const handlePurchase = useCallback(async () => {
39
49
  if (!selectedPlanId || !onPurchase) return;
40
- setIsProcessing(true);
50
+
51
+ if (__DEV__) {
52
+ console.log("[PaywallModal] handlePurchase starting:", { selectedPlanId });
53
+ }
54
+
55
+ setIsLocalProcessing(true);
56
+ startPurchase(selectedPlanId, "manual");
57
+
41
58
  try {
42
59
  const pkg = packages.find((p) => p.product.identifier === selectedPlanId);
43
- if (pkg) await onPurchase(pkg);
44
- } finally { setIsProcessing(false); }
45
- }, [selectedPlanId, packages, onPurchase]);
60
+ if (pkg) {
61
+ if (__DEV__) {
62
+ console.log("[PaywallModal] Calling onPurchase:", { productId: pkg.product.identifier });
63
+ }
64
+ await onPurchase(pkg);
65
+ if (__DEV__) {
66
+ console.log("[PaywallModal] onPurchase completed");
67
+ }
68
+ }
69
+ } finally {
70
+ setIsLocalProcessing(false);
71
+ endPurchase();
72
+ if (__DEV__) {
73
+ console.log("[PaywallModal] handlePurchase finished");
74
+ }
75
+ }
76
+ }, [selectedPlanId, packages, onPurchase, startPurchase, endPurchase]);
46
77
 
47
78
  const handleRestore = useCallback(async () => {
48
79
  if (!onRestore || isProcessing) return;
49
- setIsProcessing(true);
50
- try { await onRestore(); } finally { setIsProcessing(false); }
80
+
81
+ if (__DEV__) {
82
+ console.log("[PaywallModal] handleRestore starting");
83
+ }
84
+
85
+ setIsLocalProcessing(true);
86
+ try {
87
+ await onRestore();
88
+ if (__DEV__) {
89
+ console.log("[PaywallModal] handleRestore completed");
90
+ }
91
+ } finally {
92
+ setIsLocalProcessing(false);
93
+ }
51
94
  }, [onRestore, isProcessing]);
52
95
 
53
96
  const handleLegalUrl = useCallback(async (url: string | undefined) => {
package/src/index.ts CHANGED
@@ -32,9 +32,13 @@ export * from "./presentation/components/details/PremiumDetailsCard";
32
32
  export * from "./presentation/components/details/PremiumStatusBadge";
33
33
  export * from "./presentation/components/sections/SubscriptionSection";
34
34
  export * from "./presentation/components/feedback/PaywallFeedbackModal";
35
+ export * from "./presentation/components/overlay";
35
36
  export * from "./presentation/screens/SubscriptionDetailScreen";
36
37
  export * from "./presentation/types/SubscriptionDetailTypes";
37
38
 
39
+ // Presentation Layer - Stores
40
+ export * from "./presentation/stores";
41
+
38
42
  // Credits Domain
39
43
  export type {
40
44
  CreditType,
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Purchase Loading Overlay
3
+ * Full-screen overlay shown during purchase operations
4
+ * Locks the UI and shows a spinner with optional message
5
+ */
6
+
7
+ import React from "react";
8
+ import { View, Modal, StyleSheet } from "react-native";
9
+ import { AtomicSpinner, AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
10
+ import { usePurchaseLoadingStore, selectIsPurchasing } from "../../stores";
11
+
12
+ export interface PurchaseLoadingOverlayProps {
13
+ /** Loading message to display */
14
+ loadingText?: string;
15
+ }
16
+
17
+ export const PurchaseLoadingOverlay: React.FC<PurchaseLoadingOverlayProps> = React.memo(
18
+ ({ loadingText }) => {
19
+ const tokens = useAppDesignTokens();
20
+ const isPurchasing = usePurchaseLoadingStore(selectIsPurchasing);
21
+
22
+ if (!isPurchasing) return null;
23
+
24
+ return (
25
+ <Modal visible transparent animationType="fade" statusBarTranslucent>
26
+ <View style={[styles.container, { backgroundColor: "rgba(0, 0, 0, 0.7)" }]}>
27
+ <View style={[styles.content, { backgroundColor: tokens.colors.surface }]}>
28
+ <AtomicSpinner size="lg" color="primary" />
29
+ {loadingText && (
30
+ <AtomicText
31
+ type="bodyLarge"
32
+ style={[styles.text, { color: tokens.colors.textPrimary }]}
33
+ >
34
+ {loadingText}
35
+ </AtomicText>
36
+ )}
37
+ </View>
38
+ </View>
39
+ </Modal>
40
+ );
41
+ }
42
+ );
43
+
44
+ PurchaseLoadingOverlay.displayName = "PurchaseLoadingOverlay";
45
+
46
+ const styles = StyleSheet.create({
47
+ container: {
48
+ flex: 1,
49
+ justifyContent: "center",
50
+ alignItems: "center",
51
+ },
52
+ content: {
53
+ paddingHorizontal: 32,
54
+ paddingVertical: 24,
55
+ borderRadius: 16,
56
+ alignItems: "center",
57
+ minWidth: 200,
58
+ },
59
+ text: {
60
+ marginTop: 16,
61
+ textAlign: "center",
62
+ },
63
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Overlay Components
3
+ */
4
+
5
+ export { PurchaseLoadingOverlay, type PurchaseLoadingOverlayProps } from "./PurchaseLoadingOverlay";
@@ -7,6 +7,7 @@ import { useEffect, useRef, useCallback } from "react";
7
7
  import { getSavedPurchase, clearSavedPurchase } from "./useAuthAwarePurchase";
8
8
  import { usePremium } from "./usePremium";
9
9
  import { SubscriptionManager } from "../../revenuecat";
10
+ import { usePurchaseLoadingStore } from "../stores";
10
11
 
11
12
  declare const __DEV__: boolean;
12
13
 
@@ -26,6 +27,7 @@ export const useSavedPurchaseAutoExecution = (
26
27
  ): UseSavedPurchaseAutoExecutionResult => {
27
28
  const { userId, isAnonymous, onSuccess, onError } = params;
28
29
  const { purchasePackage } = usePremium(userId);
30
+ const { startPurchase, endPurchase } = usePurchaseLoadingStore();
29
31
 
30
32
  const prevIsAnonymousRef = useRef<boolean | undefined>(undefined);
31
33
  const isExecutingRef = useRef(false);
@@ -36,7 +38,8 @@ export const useSavedPurchaseAutoExecution = (
36
38
 
37
39
  if (__DEV__) {
38
40
  console.log(
39
- "[SavedPurchaseAutoExecution] Waiting for RevenueCat initialization..."
41
+ "[SavedPurchaseAutoExecution] Waiting for RevenueCat initialization...",
42
+ { userId: userId.slice(0, 8), productId: savedPurchase.pkg.product.identifier }
40
43
  );
41
44
  }
42
45
 
@@ -60,16 +63,26 @@ export const useSavedPurchaseAutoExecution = (
60
63
 
61
64
  if (__DEV__) {
62
65
  console.log(
63
- "[SavedPurchaseAutoExecution] RevenueCat ready, executing purchase:",
64
- pkg.product.identifier
66
+ "[SavedPurchaseAutoExecution] RevenueCat ready, starting purchase...",
67
+ { productId: pkg.product.identifier, userId: userId.slice(0, 8) }
65
68
  );
66
69
  }
67
70
 
71
+ // Start global loading state
72
+ startPurchase(pkg.product.identifier, "auto-execution");
73
+
68
74
  try {
75
+ if (__DEV__) {
76
+ console.log("[SavedPurchaseAutoExecution] Calling purchasePackage...");
77
+ }
78
+
69
79
  const success = await purchasePackage(pkg);
70
80
 
71
81
  if (__DEV__) {
72
- console.log("[SavedPurchaseAutoExecution] Purchase result:", success);
82
+ console.log("[SavedPurchaseAutoExecution] Purchase completed:", {
83
+ success,
84
+ productId: pkg.product.identifier
85
+ });
73
86
  }
74
87
 
75
88
  if (success && onSuccess) {
@@ -77,13 +90,22 @@ export const useSavedPurchaseAutoExecution = (
77
90
  }
78
91
  } catch (error) {
79
92
  if (__DEV__) {
80
- console.error("[SavedPurchaseAutoExecution] Purchase failed:", error);
93
+ console.error("[SavedPurchaseAutoExecution] Purchase error:", {
94
+ error,
95
+ productId: pkg.product.identifier
96
+ });
81
97
  }
82
98
  if (onError && error instanceof Error) {
83
99
  onError(error);
84
100
  }
85
101
  } finally {
102
+ // End global loading state
103
+ endPurchase();
86
104
  isExecutingRef.current = false;
105
+
106
+ if (__DEV__) {
107
+ console.log("[SavedPurchaseAutoExecution] Purchase flow finished");
108
+ }
87
109
  }
88
110
 
89
111
  return;
@@ -99,7 +121,7 @@ export const useSavedPurchaseAutoExecution = (
99
121
  }
100
122
  clearSavedPurchase();
101
123
  isExecutingRef.current = false;
102
- }, [userId, purchasePackage, onSuccess, onError]);
124
+ }, [userId, purchasePackage, onSuccess, onError, startPurchase, endPurchase]);
103
125
 
104
126
  useEffect(() => {
105
127
  const isAuthenticated = !!userId && !isAnonymous;
@@ -110,7 +132,7 @@ export const useSavedPurchaseAutoExecution = (
110
132
  const becameAuthenticated = wasAnonymous && isAuthenticated;
111
133
 
112
134
  if (__DEV__) {
113
- console.log("[SavedPurchaseAutoExecution] Check:", {
135
+ console.log("[SavedPurchaseAutoExecution] Auth state check:", {
114
136
  userId: userId?.slice(0, 8),
115
137
  prevIsAnonymous,
116
138
  isAnonymous,
@@ -118,11 +140,15 @@ export const useSavedPurchaseAutoExecution = (
118
140
  wasAnonymous,
119
141
  becameAuthenticated,
120
142
  hasSavedPurchase: !!savedPurchase,
143
+ savedProductId: savedPurchase?.pkg.product.identifier,
121
144
  willExecute: becameAuthenticated && !!savedPurchase && !isExecutingRef.current,
122
145
  });
123
146
  }
124
147
 
125
148
  if (becameAuthenticated && savedPurchase && !isExecutingRef.current) {
149
+ if (__DEV__) {
150
+ console.log("[SavedPurchaseAutoExecution] Triggering auto-execution...");
151
+ }
126
152
  executeWithWait();
127
153
  }
128
154
 
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Presentation Layer - Stores
3
+ */
4
+
5
+ export {
6
+ usePurchaseLoadingStore,
7
+ selectIsPurchasing,
8
+ selectPurchasingProductId,
9
+ selectPurchaseSource,
10
+ type PurchaseLoadingState,
11
+ type PurchaseLoadingActions,
12
+ type PurchaseLoadingStore,
13
+ } from "./purchaseLoadingStore";
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Purchase Loading Store
3
+ * Global state for tracking purchase loading across the app
4
+ * Used by both PaywallModal and useSavedPurchaseAutoExecution
5
+ */
6
+
7
+ import { create } from "zustand";
8
+
9
+ declare const __DEV__: boolean;
10
+
11
+ export interface PurchaseLoadingState {
12
+ /** Whether a purchase is in progress */
13
+ isPurchasing: boolean;
14
+ /** Product identifier being purchased */
15
+ purchasingProductId: string | null;
16
+ /** Source of the purchase (manual, auto-execution, etc.) */
17
+ purchaseSource: "manual" | "auto-execution" | null;
18
+ }
19
+
20
+ export interface PurchaseLoadingActions {
21
+ /** Start purchase loading state */
22
+ startPurchase: (productId: string, source: "manual" | "auto-execution") => void;
23
+ /** End purchase loading state */
24
+ endPurchase: () => void;
25
+ /** Reset all state */
26
+ reset: () => void;
27
+ }
28
+
29
+ export type PurchaseLoadingStore = PurchaseLoadingState & PurchaseLoadingActions;
30
+
31
+ const initialState: PurchaseLoadingState = {
32
+ isPurchasing: false,
33
+ purchasingProductId: null,
34
+ purchaseSource: null,
35
+ };
36
+
37
+ export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set) => ({
38
+ ...initialState,
39
+
40
+ startPurchase: (productId, source) => {
41
+ if (__DEV__) {
42
+ console.log("[PurchaseLoadingStore] startPurchase:", { productId, source });
43
+ }
44
+ set({
45
+ isPurchasing: true,
46
+ purchasingProductId: productId,
47
+ purchaseSource: source,
48
+ });
49
+ },
50
+
51
+ endPurchase: () => {
52
+ if (__DEV__) {
53
+ console.log("[PurchaseLoadingStore] endPurchase");
54
+ }
55
+ set({
56
+ isPurchasing: false,
57
+ purchasingProductId: null,
58
+ purchaseSource: null,
59
+ });
60
+ },
61
+
62
+ reset: () => {
63
+ if (__DEV__) {
64
+ console.log("[PurchaseLoadingStore] reset");
65
+ }
66
+ set(initialState);
67
+ },
68
+ }));
69
+
70
+ // Selectors for optimized re-renders
71
+ export const selectIsPurchasing = (state: PurchaseLoadingStore) => state.isPurchasing;
72
+ export const selectPurchasingProductId = (state: PurchaseLoadingStore) => state.purchasingProductId;
73
+ export const selectPurchaseSource = (state: PurchaseLoadingStore) => state.purchaseSource;
@@ -65,11 +65,26 @@ export async function handlePurchase(
65
65
 
66
66
  try {
67
67
  if (__DEV__) {
68
- console.log('[DEBUG PurchaseHandler] Calling Purchases.purchasePackage...');
68
+ console.log('[DEBUG PurchaseHandler] Calling Purchases.purchasePackage...', {
69
+ productId: pkg.product.identifier,
70
+ packageIdentifier: pkg.identifier,
71
+ offeringIdentifier: pkg.offeringIdentifier,
72
+ timestamp: new Date().toISOString()
73
+ });
69
74
  }
75
+
76
+ const startTime = Date.now();
70
77
  const purchaseResult = await Purchases.purchasePackage(pkg);
78
+ const duration = Date.now() - startTime;
71
79
  const customerInfo = purchaseResult.customerInfo;
72
80
 
81
+ if (__DEV__) {
82
+ console.log('[DEBUG PurchaseHandler] Purchases.purchasePackage returned', {
83
+ duration: `${duration}ms`,
84
+ productId: pkg.product.identifier
85
+ });
86
+ }
87
+
73
88
  if (__DEV__) {
74
89
  console.log('[DEBUG PurchaseHandler] Purchase completed', {
75
90
  productId: pkg.product.identifier,