@umituz/react-native-subscription 2.41.0 → 2.41.3

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.41.0",
3
+ "version": "2.41.3",
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",
@@ -51,12 +51,6 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
51
51
  const tokens = useAppDesignTokens();
52
52
  const insets = useSafeAreaInsets();
53
53
 
54
- // Defensive check for translations to prevent crashes
55
- if (!translations) {
56
- if (__DEV__) console.warn("[PaywallScreen] Translations prop is missing");
57
- return null;
58
- }
59
-
60
54
  const {
61
55
  selectedPlanId,
62
56
  setSelectedPlanId,
@@ -125,6 +119,7 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
125
119
  }, [features, packages]);
126
120
 
127
121
  const renderItem: ListRenderItem<PaywallListItem> = useCallback(({ item }) => {
122
+ if (!translations) return null;
128
123
  switch (item.type) {
129
124
  case 'HEADER':
130
125
  return (
@@ -200,6 +195,7 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
200
195
  }
201
196
  }, [heroImage, translations, tokens, selectedPlanId, bestValueIdentifier, creditAmounts, creditsLabel, setSelectedPlanId]);
202
197
 
198
+
203
199
  // Performance Optimization: getItemLayout for FlatList
204
200
  const getItemLayout = useCallback((_data: any, index: number) => {
205
201
  return calculatePaywallItemLayout(flatData, index);
@@ -211,6 +207,12 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
211
207
  return `${item.type}-${index}`;
212
208
  }, []);
213
209
 
210
+ // Defensive check for translations moved to the end of hooks
211
+ if (!translations) {
212
+ if (__DEV__) console.warn("[PaywallScreen] Translations prop is missing");
213
+ return null;
214
+ }
215
+
214
216
  if (isLoadingPackages) {
215
217
  return (
216
218
  <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary, paddingTop: insets.top }]}>
@@ -4,6 +4,7 @@ import { getCreditsRepository } from "../../credits/infrastructure/CreditsReposi
4
4
  import { extractRevenueCatData } from "./SubscriptionSyncUtils";
5
5
  import { generatePurchaseId, generateRenewalId } from "./syncIdGenerators";
6
6
  import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
7
+ import { useSubscriptionFlowStore, SyncStatus } from "../presentation/useSubscriptionFlow";
7
8
 
8
9
  /**
9
10
  * Central processor for all subscription sync operations.
@@ -26,6 +27,9 @@ export class SubscriptionSyncProcessor {
26
27
  // ─── Public API (replaces SubscriptionSyncService) ────────────────
27
28
 
28
29
  async handlePurchase(event: PurchaseCompletedEvent): Promise<void> {
30
+ const store = useSubscriptionFlowStore.getState();
31
+ store.setSyncStatus(SyncStatus.SYNCING);
32
+
29
33
  if (typeof __DEV__ !== "undefined" && __DEV__) {
30
34
  console.log('[SubscriptionSyncProcessor] 🔵 PURCHASE START', {
31
35
  userId: event.userId,
@@ -41,6 +45,7 @@ export class SubscriptionSyncProcessor {
41
45
  userId: event.userId,
42
46
  productId: event.productId,
43
47
  });
48
+ store.setSyncStatus(SyncStatus.SUCCESS);
44
49
  if (typeof __DEV__ !== "undefined" && __DEV__) {
45
50
  console.log('[SubscriptionSyncProcessor] 🟢 PURCHASE SUCCESS', {
46
51
  userId: event.userId,
@@ -49,10 +54,12 @@ export class SubscriptionSyncProcessor {
49
54
  });
50
55
  }
51
56
  } catch (error) {
57
+ const errorMsg = error instanceof Error ? error.message : String(error);
58
+ store.setSyncStatus(SyncStatus.ERROR, errorMsg);
52
59
  console.error('[SubscriptionSyncProcessor] 🔴 PURCHASE FAILED', {
53
60
  userId: event.userId,
54
61
  productId: event.productId,
55
- error: error instanceof Error ? error.message : String(error),
62
+ error: errorMsg,
56
63
  timestamp: new Date().toISOString(),
57
64
  });
58
65
  throw error;
@@ -60,6 +67,9 @@ export class SubscriptionSyncProcessor {
60
67
  }
61
68
 
62
69
  async handleRenewal(event: RenewalDetectedEvent): Promise<void> {
70
+ const store = useSubscriptionFlowStore.getState();
71
+ store.setSyncStatus(SyncStatus.SYNCING);
72
+
63
73
  if (typeof __DEV__ !== "undefined" && __DEV__) {
64
74
  console.log('[SubscriptionSyncProcessor] 🔵 RENEWAL START', {
65
75
  userId: event.userId,
@@ -74,6 +84,7 @@ export class SubscriptionSyncProcessor {
74
84
  userId: event.userId,
75
85
  productId: event.productId,
76
86
  });
87
+ store.setSyncStatus(SyncStatus.SUCCESS);
77
88
  if (typeof __DEV__ !== "undefined" && __DEV__) {
78
89
  console.log('[SubscriptionSyncProcessor] 🟢 RENEWAL SUCCESS', {
79
90
  userId: event.userId,
@@ -82,10 +93,12 @@ export class SubscriptionSyncProcessor {
82
93
  });
83
94
  }
84
95
  } catch (error) {
96
+ const errorMsg = error instanceof Error ? error.message : String(error);
97
+ store.setSyncStatus(SyncStatus.ERROR, errorMsg);
85
98
  console.error('[SubscriptionSyncProcessor] 🔴 RENEWAL FAILED', {
86
99
  userId: event.userId,
87
100
  productId: event.productId,
88
- error: error instanceof Error ? error.message : String(error),
101
+ error: errorMsg,
89
102
  timestamp: new Date().toISOString(),
90
103
  });
91
104
  throw error;
@@ -118,14 +131,16 @@ export class SubscriptionSyncProcessor {
118
131
  });
119
132
  }
120
133
  } catch (error) {
134
+ const errorMsg = error instanceof Error ? error.message : String(error);
121
135
  console.error('[SubscriptionSyncProcessor] 🔴 STATUS CHANGE FAILED', {
122
136
  userId: event.userId,
123
137
  isPremium: event.isPremium,
124
138
  productId: event.productId,
125
- error: error instanceof Error ? error.message : String(error),
139
+ error: errorMsg,
126
140
  timestamp: new Date().toISOString(),
127
141
  });
128
- throw error;
142
+ // We don't set global sync error here for passive status changes to avoid UI noise
143
+ // throw error;
129
144
  }
130
145
  }
131
146
 
@@ -70,7 +70,13 @@ export interface ManagedSubscriptionFlowProps {
70
70
  *
71
71
  * Use this to reduce AppNavigator boilerplate to nearly zero.
72
72
  */
73
- export const ManagedSubscriptionFlow: React.FC<ManagedSubscriptionFlowProps> = ({
73
+ import {
74
+ SubscriptionFlowProvider,
75
+ useSubscriptionFlowStatus
76
+ } from "../providers/SubscriptionFlowProvider";
77
+ import { SubscriptionFlowStatus } from "../useSubscriptionFlow";
78
+
79
+ const ManagedSubscriptionFlowInner: React.FC<ManagedSubscriptionFlowProps> = ({
74
80
  children,
75
81
  navigation,
76
82
  islocalizationReady,
@@ -81,6 +87,7 @@ export const ManagedSubscriptionFlow: React.FC<ManagedSubscriptionFlowProps> = (
81
87
  offline,
82
88
  }) => {
83
89
  const tokens = useAppDesignTokens();
90
+ const status = useSubscriptionFlowStatus();
84
91
  const { isInitialized: isSplashComplete } = useSplashFlow({
85
92
  duration: splash?.duration || 0,
86
93
  });
@@ -123,24 +130,38 @@ export const ManagedSubscriptionFlow: React.FC<ManagedSubscriptionFlowProps> = (
123
130
  }
124
131
  };
125
132
 
126
- // 1. Loading / Splash State
127
- if (splash && (!isSplashComplete || !flowState.isInitialized || !islocalizationReady)) {
133
+ // 1. Loading / Initialization View
134
+ if (status === SubscriptionFlowStatus.INITIALIZING || !islocalizationReady) {
135
+ if (__DEV__) {
136
+ console.log('[ManagedSubscriptionFlow] ⏳ Rendering Initialization state', {
137
+ status,
138
+ islocalizationReady,
139
+ hasSplashConfig: !!splash,
140
+ isSplashComplete
141
+ });
142
+ }
143
+
144
+ // Even if no splash config provided, we should show a basic splash to avoid white screen
128
145
  return (
129
146
  <SplashScreen
130
- appName={splash.appName}
131
- tagline={splash.tagline}
147
+ appName={splash?.appName || "Loading..."}
148
+ tagline={splash?.tagline || "Please wait while we set things up"}
132
149
  colors={tokens.colors}
133
150
  />
134
151
  );
135
152
  }
136
153
 
137
- // If no splash and not ready, show minimal loading
138
- if (!splash && (!flowState.isInitialized || !islocalizationReady)) {
139
- return null;
154
+ if (__DEV__) {
155
+ console.log('[ManagedSubscriptionFlow] 🔄 Rendering Main state', {
156
+ status,
157
+ isSplashComplete,
158
+ islocalizationReady,
159
+ showFeedback: flowState.showFeedback
160
+ });
140
161
  }
141
162
 
142
- // 2. Onboarding State
143
- if (!flowState.isOnboardingComplete) {
163
+ // 2. Onboarding View
164
+ if (status === SubscriptionFlowStatus.ONBOARDING) {
144
165
  return (
145
166
  <OnboardingScreen
146
167
  slides={onboarding.slides}
@@ -154,7 +175,7 @@ export const ManagedSubscriptionFlow: React.FC<ManagedSubscriptionFlowProps> = (
154
175
  );
155
176
  }
156
177
 
157
- // 3. Main Application State
178
+ // 3. Application Content + Overlays
158
179
  return (
159
180
  <>
160
181
  {children}
@@ -178,3 +199,11 @@ export const ManagedSubscriptionFlow: React.FC<ManagedSubscriptionFlowProps> = (
178
199
  </>
179
200
  );
180
201
  };
202
+
203
+ export const ManagedSubscriptionFlow: React.FC<ManagedSubscriptionFlowProps> = (props) => {
204
+ return (
205
+ <SubscriptionFlowProvider>
206
+ <ManagedSubscriptionFlowInner {...props} />
207
+ </SubscriptionFlowProvider>
208
+ );
209
+ };
@@ -0,0 +1,85 @@
1
+ import React, { createContext, useContext, useEffect } from "react";
2
+ import { useSubscriptionFlowStore, SubscriptionFlowStatus } from "../useSubscriptionFlow";
3
+ import { initializationState } from "../../infrastructure/state/initializationState";
4
+
5
+ interface SubscriptionFlowContextType {
6
+ status: SubscriptionFlowStatus;
7
+ }
8
+
9
+ const SubscriptionFlowContext = createContext<SubscriptionFlowContextType | undefined>(undefined);
10
+
11
+ export const SubscriptionFlowProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
12
+ const store = useSubscriptionFlowStore();
13
+
14
+ useEffect(() => {
15
+ // 1. Listen to background initialization state
16
+ const unsubscribe = initializationState.subscribe(() => {
17
+ const { initialized } = initializationState.getSnapshot();
18
+ if (__DEV__) {
19
+ console.log('[SubscriptionFlowProvider] 🔄 Initialization state updated:', { initialized });
20
+ }
21
+ if (initialized && !store.isInitialized) {
22
+ store.setInitialized(true);
23
+ }
24
+ });
25
+
26
+ // Check initial state
27
+ const { initialized } = initializationState.getSnapshot();
28
+ if (initialized && !store.isInitialized) {
29
+ if (__DEV__) console.log('[SubscriptionFlowProvider] ✅ Already initialized on mount');
30
+ store.setInitialized(true);
31
+ }
32
+
33
+ return () => unsubscribe();
34
+ }, [store.isInitialized, store.setInitialized]);
35
+
36
+ useEffect(() => {
37
+ // This effect manages the overall flow status transition
38
+ if (__DEV__) {
39
+ console.log('[SubscriptionFlowProvider] 🧠 Calculating Status Transition', {
40
+ isInitialized: store.isInitialized,
41
+ isOnboardingComplete: store.isOnboardingComplete,
42
+ showPostOnboardingPaywall: store.showPostOnboardingPaywall,
43
+ currentStatus: store.status
44
+ });
45
+ }
46
+
47
+ if (!store.isInitialized) {
48
+ store.setStatus(SubscriptionFlowStatus.INITIALIZING);
49
+ return;
50
+ }
51
+
52
+ if (!store.isOnboardingComplete) {
53
+ store.setStatus(SubscriptionFlowStatus.ONBOARDING);
54
+ return;
55
+ }
56
+
57
+ if (store.showPostOnboardingPaywall) {
58
+ store.setStatus(SubscriptionFlowStatus.POST_ONBOARDING_PAYWALL);
59
+ return;
60
+ }
61
+
62
+ if (__DEV__) console.log('[SubscriptionFlowProvider] 🏆 Flow is READY');
63
+ store.setStatus(SubscriptionFlowStatus.READY);
64
+ }, [
65
+ store.isInitialized,
66
+ store.isOnboardingComplete,
67
+ store.showPostOnboardingPaywall,
68
+ store.paywallShown,
69
+ store
70
+ ]);
71
+
72
+ return (
73
+ <SubscriptionFlowContext.Provider value={{ status: store.status }}>
74
+ {children}
75
+ </SubscriptionFlowContext.Provider>
76
+ );
77
+ };
78
+
79
+ export const useSubscriptionFlowStatus = () => {
80
+ const context = useContext(SubscriptionFlowContext);
81
+ if (context === undefined) {
82
+ throw new Error("useSubscriptionFlowStatus must be used within a SubscriptionFlowProvider");
83
+ }
84
+ return context.status;
85
+ };
@@ -7,7 +7,25 @@
7
7
  import { DeviceEventEmitter } from "react-native";
8
8
  import { createStore } from "@umituz/react-native-design-system/storage";
9
9
 
10
+ export enum SubscriptionFlowStatus {
11
+ INITIALIZING = "INITIALIZING",
12
+ ONBOARDING = "ONBOARDING",
13
+ PAYWALL = "PAYWALL",
14
+ READY = "READY",
15
+ POST_ONBOARDING_PAYWALL = "POST_ONBOARDING_PAYWALL",
16
+ }
17
+
18
+ export enum SyncStatus {
19
+ IDLE = "IDLE",
20
+ SYNCING = "SYNCING",
21
+ SUCCESS = "SUCCESS",
22
+ ERROR = "ERROR",
23
+ }
24
+
10
25
  export interface SubscriptionFlowState {
26
+ status: SubscriptionFlowStatus;
27
+ syncStatus: SyncStatus;
28
+ syncError: string | null;
11
29
  isInitialized: boolean;
12
30
  isOnboardingComplete: boolean;
13
31
  showPostOnboardingPaywall: boolean;
@@ -25,11 +43,16 @@ export interface SubscriptionFlowActions {
25
43
  setShowFeedback: (show: boolean) => void;
26
44
  resetFlow: () => Promise<void>;
27
45
  setInitialized: (initialized: boolean) => void;
46
+ setStatus: (status: SubscriptionFlowStatus) => void;
47
+ setSyncStatus: (status: SyncStatus, error?: string | null) => void;
28
48
  }
29
49
 
30
50
  export type SubscriptionFlowStore = SubscriptionFlowState & SubscriptionFlowActions;
31
51
 
32
52
  const initialState: SubscriptionFlowState = {
53
+ status: SubscriptionFlowStatus.INITIALIZING,
54
+ syncStatus: SyncStatus.IDLE,
55
+ syncError: null,
33
56
  isInitialized: false,
34
57
  isOnboardingComplete: false,
35
58
  showPostOnboardingPaywall: false,
@@ -55,6 +78,7 @@ export const useSubscriptionFlowStore = createStore<SubscriptionFlowState, Subsc
55
78
  set({
56
79
  isOnboardingComplete: true,
57
80
  showPostOnboardingPaywall: true,
81
+ status: SubscriptionFlowStatus.POST_ONBOARDING_PAYWALL,
58
82
  });
59
83
  DeviceEventEmitter.emit("onboarding-complete");
60
84
  },
@@ -62,6 +86,7 @@ export const useSubscriptionFlowStore = createStore<SubscriptionFlowState, Subsc
62
86
  set({
63
87
  showPostOnboardingPaywall: false,
64
88
  paywallShown: true,
89
+ status: SubscriptionFlowStatus.READY,
65
90
  });
66
91
  },
67
92
  closeFeedback: () => set({ showFeedback: false }),
@@ -69,8 +94,14 @@ export const useSubscriptionFlowStore = createStore<SubscriptionFlowState, Subsc
69
94
  setShowFeedback: (show: boolean) => set({ showFeedback: show }),
70
95
  markPaywallShown: async () => set({ paywallShown: true }),
71
96
  setInitialized: (initialized: boolean) => set({ isInitialized: initialized }),
97
+ setStatus: (status: SubscriptionFlowStatus) => set({ status }),
98
+ setSyncStatus: (syncStatus: SyncStatus, syncError: string | null = null) =>
99
+ set({ syncStatus, syncError }),
72
100
  resetFlow: async () => {
73
101
  set({
102
+ status: SubscriptionFlowStatus.INITIALIZING,
103
+ syncStatus: SyncStatus.IDLE,
104
+ syncError: null,
74
105
  isOnboardingComplete: false,
75
106
  showPostOnboardingPaywall: false,
76
107
  showFeedback: false,
package/src/index.ts CHANGED
@@ -97,6 +97,8 @@ export { createPaywallTranslations, createFeedbackTranslations } from "./domains
97
97
  // Root Flow Components
98
98
  export { ManagedSubscriptionFlow } from "./domains/subscription/presentation/components/ManagedSubscriptionFlow";
99
99
  export type { ManagedSubscriptionFlowProps } from "./domains/subscription/presentation/components/ManagedSubscriptionFlow";
100
+ export { SubscriptionFlowStatus } from "./domains/subscription/presentation/useSubscriptionFlow";
101
+ export { SubscriptionFlowProvider, useSubscriptionFlowStatus } from "./domains/subscription/presentation/providers/SubscriptionFlowProvider";
100
102
 
101
103
  // Purchase Loading Overlay
102
104
  export { PurchaseLoadingOverlay } from "./domains/subscription/presentation/components/overlay/PurchaseLoadingOverlay";