@umituz/react-native-subscription 2.41.0 → 2.41.2

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.2",
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,22 @@ export const ManagedSubscriptionFlow: React.FC<ManagedSubscriptionFlowProps> = (
123
130
  }
124
131
  };
125
132
 
126
- // 1. Loading / Splash State
127
- if (splash && (!isSplashComplete || !flowState.isInitialized || !islocalizationReady)) {
128
- return (
129
- <SplashScreen
130
- appName={splash.appName}
131
- tagline={splash.tagline}
132
- colors={tokens.colors}
133
- />
134
- );
135
- }
136
-
137
- // If no splash and not ready, show minimal loading
138
- if (!splash && (!flowState.isInitialized || !islocalizationReady)) {
133
+ // 1. Loading / Splash View
134
+ if (status === SubscriptionFlowStatus.INITIALIZING || !islocalizationReady) {
135
+ if (splash && (!isSplashComplete || !islocalizationReady)) {
136
+ return (
137
+ <SplashScreen
138
+ appName={splash.appName}
139
+ tagline={splash.tagline}
140
+ colors={tokens.colors}
141
+ />
142
+ );
143
+ }
139
144
  return null;
140
145
  }
141
146
 
142
- // 2. Onboarding State
143
- if (!flowState.isOnboardingComplete) {
147
+ // 2. Onboarding View
148
+ if (status === SubscriptionFlowStatus.ONBOARDING) {
144
149
  return (
145
150
  <OnboardingScreen
146
151
  slides={onboarding.slides}
@@ -154,7 +159,7 @@ export const ManagedSubscriptionFlow: React.FC<ManagedSubscriptionFlowProps> = (
154
159
  );
155
160
  }
156
161
 
157
- // 3. Main Application State
162
+ // 3. Application Content + Overlays
158
163
  return (
159
164
  <>
160
165
  {children}
@@ -178,3 +183,11 @@ export const ManagedSubscriptionFlow: React.FC<ManagedSubscriptionFlowProps> = (
178
183
  </>
179
184
  );
180
185
  };
186
+
187
+ export const ManagedSubscriptionFlow: React.FC<ManagedSubscriptionFlowProps> = (props) => {
188
+ return (
189
+ <SubscriptionFlowProvider>
190
+ <ManagedSubscriptionFlowInner {...props} />
191
+ </SubscriptionFlowProvider>
192
+ );
193
+ };
@@ -0,0 +1,52 @@
1
+ import React, { createContext, useContext, useEffect } from "react";
2
+ import { useSubscriptionFlowStore, SubscriptionFlowStatus } from "../useSubscriptionFlow";
3
+
4
+ interface SubscriptionFlowContextType {
5
+ status: SubscriptionFlowStatus;
6
+ }
7
+
8
+ const SubscriptionFlowContext = createContext<SubscriptionFlowContextType | undefined>(undefined);
9
+
10
+ export const SubscriptionFlowProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
11
+ const store = useSubscriptionFlowStore();
12
+
13
+ useEffect(() => {
14
+ // This effect manages the overall flow status transition
15
+ if (!store.isInitialized) {
16
+ store.setStatus(SubscriptionFlowStatus.INITIALIZING);
17
+ return;
18
+ }
19
+
20
+ if (!store.isOnboardingComplete) {
21
+ store.setStatus(SubscriptionFlowStatus.ONBOARDING);
22
+ return;
23
+ }
24
+
25
+ if (store.showPostOnboardingPaywall) {
26
+ store.setStatus(SubscriptionFlowStatus.POST_ONBOARDING_PAYWALL);
27
+ return;
28
+ }
29
+
30
+ store.setStatus(SubscriptionFlowStatus.READY);
31
+ }, [
32
+ store.isInitialized,
33
+ store.isOnboardingComplete,
34
+ store.showPostOnboardingPaywall,
35
+ store.paywallShown,
36
+ store
37
+ ]);
38
+
39
+ return (
40
+ <SubscriptionFlowContext.Provider value={{ status: store.status }}>
41
+ {children}
42
+ </SubscriptionFlowContext.Provider>
43
+ );
44
+ };
45
+
46
+ export const useSubscriptionFlowStatus = () => {
47
+ const context = useContext(SubscriptionFlowContext);
48
+ if (context === undefined) {
49
+ throw new Error("useSubscriptionFlowStatus must be used within a SubscriptionFlowProvider");
50
+ }
51
+ return context.status;
52
+ };
@@ -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";