@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 +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 +40 -11
- package/src/domains/subscription/presentation/providers/SubscriptionFlowProvider.tsx +85 -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.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:
|
|
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,38 @@ export const ManagedSubscriptionFlow: React.FC<ManagedSubscriptionFlowProps> = (
|
|
|
123
130
|
}
|
|
124
131
|
};
|
|
125
132
|
|
|
126
|
-
// 1. Loading /
|
|
127
|
-
if (
|
|
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
|
|
131
|
-
tagline={splash
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
143
|
-
if (
|
|
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.
|
|
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";
|