@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 +1 -1
- package/src/domains/paywall/components/PaywallScreen.tsx +8 -6
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +19 -4
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.tsx +30 -17
- package/src/domains/subscription/presentation/providers/SubscriptionFlowProvider.tsx +52 -0
- package/src/domains/subscription/presentation/useSubscriptionFlow.ts +31 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.41.
|
|
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:
|
|
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:
|
|
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:
|
|
139
|
+
error: errorMsg,
|
|
126
140
|
timestamp: new Date().toISOString(),
|
|
127
141
|
});
|
|
128
|
-
|
|
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
|
-
|
|
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
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
143
|
-
if (
|
|
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.
|
|
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";
|