@umituz/react-native-subscription 2.40.13 → 2.40.14
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.40.
|
|
3
|
+
"version": "2.40.14",
|
|
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",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"url": "git+https://github.com/umituz/react-native-subscription.git"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
+
"@react-native-async-storage/async-storage": ">=2.0.0",
|
|
41
42
|
"@tanstack/react-query": ">=5.0.0",
|
|
42
43
|
"@umituz/react-native-auth": "*",
|
|
43
44
|
"expo": ">=54.0.0",
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import { usePremium } from "../../subscription/presentation/usePremium";
|
|
3
|
+
import { useSubscriptionFlow } from "../../subscription/presentation/useSubscriptionFlow";
|
|
4
|
+
import { usePaywallVisibility } from "../../subscription/presentation/usePaywallVisibility";
|
|
5
|
+
import { PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "../entities/types";
|
|
6
|
+
|
|
7
|
+
export interface PaywallOrchestratorOptions {
|
|
8
|
+
navigation: any;
|
|
9
|
+
translations: PaywallTranslations;
|
|
10
|
+
features: SubscriptionFeature[];
|
|
11
|
+
legalUrls: PaywallLegalUrls;
|
|
12
|
+
heroImage: any;
|
|
13
|
+
isNavReady?: boolean;
|
|
14
|
+
isLocalizationReady?: boolean;
|
|
15
|
+
onAuthRequired?: () => void;
|
|
16
|
+
onPurchaseSuccess?: () => void;
|
|
17
|
+
bestValueIdentifier?: string;
|
|
18
|
+
creditsLabel?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* High-level orchestrator for Paywall navigation.
|
|
23
|
+
* Handles automatic triggers (post-onboarding) and manual triggers (showPaywall state).
|
|
24
|
+
* Centralizes handlers for success, close, and feedback triggers.
|
|
25
|
+
*/
|
|
26
|
+
export function usePaywallOrchestrator({
|
|
27
|
+
navigation,
|
|
28
|
+
translations,
|
|
29
|
+
features,
|
|
30
|
+
legalUrls,
|
|
31
|
+
heroImage,
|
|
32
|
+
isNavReady = true,
|
|
33
|
+
isLocalizationReady = true,
|
|
34
|
+
onAuthRequired,
|
|
35
|
+
onPurchaseSuccess,
|
|
36
|
+
bestValueIdentifier = "yearly",
|
|
37
|
+
creditsLabel,
|
|
38
|
+
}: PaywallOrchestratorOptions) {
|
|
39
|
+
const {
|
|
40
|
+
isPremium,
|
|
41
|
+
packages,
|
|
42
|
+
purchasePackage,
|
|
43
|
+
restorePurchase
|
|
44
|
+
} = usePremium();
|
|
45
|
+
|
|
46
|
+
const {
|
|
47
|
+
state,
|
|
48
|
+
markPaywallShown,
|
|
49
|
+
closePostOnboardingPaywall,
|
|
50
|
+
setShowFeedback
|
|
51
|
+
} = useSubscriptionFlow();
|
|
52
|
+
|
|
53
|
+
const { showPaywall, closePaywall } = usePaywallVisibility();
|
|
54
|
+
const purchasedRef = useRef(false);
|
|
55
|
+
|
|
56
|
+
const handleClose = useCallback(async () => {
|
|
57
|
+
await closePostOnboardingPaywall();
|
|
58
|
+
closePaywall();
|
|
59
|
+
|
|
60
|
+
// Trigger feedback if user declined and isn't premium
|
|
61
|
+
if (!isPremium && !purchasedRef.current) {
|
|
62
|
+
setTimeout(() => setShowFeedback(true), 300);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
purchasedRef.current = false;
|
|
66
|
+
|
|
67
|
+
if (navigation.canGoBack()) {
|
|
68
|
+
navigation.goBack();
|
|
69
|
+
}
|
|
70
|
+
}, [closePostOnboardingPaywall, closePaywall, isPremium, navigation, setShowFeedback]);
|
|
71
|
+
|
|
72
|
+
const handleSuccess = useCallback(async () => {
|
|
73
|
+
purchasedRef.current = true;
|
|
74
|
+
await markPaywallShown();
|
|
75
|
+
await closePostOnboardingPaywall();
|
|
76
|
+
|
|
77
|
+
onPurchaseSuccess?.();
|
|
78
|
+
|
|
79
|
+
if (navigation.canGoBack()) {
|
|
80
|
+
navigation.goBack();
|
|
81
|
+
}
|
|
82
|
+
}, [markPaywallShown, closePostOnboardingPaywall, onPurchaseSuccess, navigation]);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!isNavReady || !isLocalizationReady) return;
|
|
86
|
+
|
|
87
|
+
const shouldShowPostOnboarding =
|
|
88
|
+
state.isOnboardingComplete &&
|
|
89
|
+
state.showPostOnboardingPaywall &&
|
|
90
|
+
!state.paywallShown &&
|
|
91
|
+
!state.isAuthModalOpen &&
|
|
92
|
+
!isPremium;
|
|
93
|
+
|
|
94
|
+
const shouldShowManual = showPaywall && !isPremium && !state.isAuthModalOpen;
|
|
95
|
+
|
|
96
|
+
if (shouldShowPostOnboarding || shouldShowManual) {
|
|
97
|
+
navigation.navigate("PaywallScreen", {
|
|
98
|
+
onClose: handleClose,
|
|
99
|
+
translations,
|
|
100
|
+
legalUrls,
|
|
101
|
+
features,
|
|
102
|
+
bestValueIdentifier,
|
|
103
|
+
creditsLabel,
|
|
104
|
+
onAuthRequired,
|
|
105
|
+
onPurchaseSuccess: handleSuccess,
|
|
106
|
+
heroImage,
|
|
107
|
+
source: shouldShowPostOnboarding ? "onboarding" : "manual",
|
|
108
|
+
packages,
|
|
109
|
+
onPurchase: purchasePackage,
|
|
110
|
+
onRestore: restorePurchase,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (shouldShowPostOnboarding) {
|
|
114
|
+
markPaywallShown();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (showPaywall) {
|
|
118
|
+
closePaywall();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}, [
|
|
122
|
+
isNavReady,
|
|
123
|
+
isLocalizationReady,
|
|
124
|
+
state.isOnboardingComplete,
|
|
125
|
+
state.showPostOnboardingPaywall,
|
|
126
|
+
state.paywallShown,
|
|
127
|
+
state.isAuthModalOpen,
|
|
128
|
+
isPremium,
|
|
129
|
+
showPaywall,
|
|
130
|
+
navigation,
|
|
131
|
+
handleClose,
|
|
132
|
+
handleSuccess,
|
|
133
|
+
translations,
|
|
134
|
+
legalUrls,
|
|
135
|
+
features,
|
|
136
|
+
heroImage,
|
|
137
|
+
packages,
|
|
138
|
+
purchasePackage,
|
|
139
|
+
restorePurchase,
|
|
140
|
+
markPaywallShown,
|
|
141
|
+
closePaywall,
|
|
142
|
+
bestValueIdentifier,
|
|
143
|
+
creditsLabel,
|
|
144
|
+
onAuthRequired
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
isPremium,
|
|
149
|
+
packages,
|
|
150
|
+
flowState: state,
|
|
151
|
+
markPaywallShown,
|
|
152
|
+
closePostOnboardingPaywall,
|
|
153
|
+
setShowFeedback
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PaywallTranslations } from "../entities/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates standardized paywall translations from a translation function.
|
|
5
|
+
* Matches the structure used across the App Factory ecosystem.
|
|
6
|
+
*/
|
|
7
|
+
export const createPaywallTranslations = (
|
|
8
|
+
t: (key: string) => string,
|
|
9
|
+
overrides?: Partial<PaywallTranslations>
|
|
10
|
+
): PaywallTranslations => ({
|
|
11
|
+
title: t("premium.title"),
|
|
12
|
+
subtitle: t("premium.subtitle"),
|
|
13
|
+
loadingText: t("paywall.loading"),
|
|
14
|
+
emptyText: t("paywall.empty"),
|
|
15
|
+
purchaseButtonText: t("paywall.purchase"),
|
|
16
|
+
restoreButtonText: t("paywall.restore"),
|
|
17
|
+
processingText: t("paywall.processing"),
|
|
18
|
+
privacyText: t("auth.privacyPolicy"),
|
|
19
|
+
termsOfServiceText: t("auth.termsOfService"),
|
|
20
|
+
bestValueBadgeText: t("paywall.bestValue"),
|
|
21
|
+
featuresTitle: t("paywall.featuresTitle"),
|
|
22
|
+
plansTitle: t("paywall.plansTitle"),
|
|
23
|
+
...overrides,
|
|
24
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Flow Hook
|
|
3
|
+
* Manages the high-level application flow: Splash -> Onboarding -> Paywall -> Main App.
|
|
4
|
+
* Centralizing this in the library allows for minimal boilerplate in the main application's navigator.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useEffect, useCallback } from "react";
|
|
8
|
+
import { DeviceEventEmitter } from "react-native";
|
|
9
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
10
|
+
|
|
11
|
+
const PAYWALL_SHOWN_KEY = "post_onboarding_paywall_shown";
|
|
12
|
+
const ONBOARDING_KEY = "onboarding_complete";
|
|
13
|
+
|
|
14
|
+
export interface SubscriptionFlowState {
|
|
15
|
+
isInitialized: boolean;
|
|
16
|
+
isOnboardingComplete: boolean;
|
|
17
|
+
showPostOnboardingPaywall: boolean;
|
|
18
|
+
showFeedback: boolean;
|
|
19
|
+
paywallShown: boolean;
|
|
20
|
+
isAuthModalOpen: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseSubscriptionFlowResult {
|
|
24
|
+
state: SubscriptionFlowState;
|
|
25
|
+
completeOnboarding: () => Promise<void>;
|
|
26
|
+
closePostOnboardingPaywall: () => Promise<void>;
|
|
27
|
+
closeFeedback: () => void;
|
|
28
|
+
setAuthModalOpen: (open: boolean) => void;
|
|
29
|
+
markPaywallShown: () => Promise<void>;
|
|
30
|
+
setShowFeedback: (show: boolean) => void;
|
|
31
|
+
resetFlow: () => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useSubscriptionFlow(userId?: string): UseSubscriptionFlowResult {
|
|
35
|
+
const [state, setState] = useState<SubscriptionFlowState>({
|
|
36
|
+
isInitialized: false,
|
|
37
|
+
isOnboardingComplete: false,
|
|
38
|
+
showPostOnboardingPaywall: false,
|
|
39
|
+
showFeedback: false,
|
|
40
|
+
paywallShown: false,
|
|
41
|
+
isAuthModalOpen: false,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Initialization: Load persisted state
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const init = async () => {
|
|
47
|
+
try {
|
|
48
|
+
const [onboardingValue, paywallValue] = await Promise.all([
|
|
49
|
+
AsyncStorage.getItem(ONBOARDING_KEY),
|
|
50
|
+
AsyncStorage.getItem(PAYWALL_SHOWN_KEY),
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
setState((prev) => ({
|
|
54
|
+
...prev,
|
|
55
|
+
isInitialized: true,
|
|
56
|
+
isOnboardingComplete: onboardingValue === "true",
|
|
57
|
+
paywallShown: paywallValue === "true",
|
|
58
|
+
}));
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error("[useSubscriptionFlow] Initialization failed:", error);
|
|
61
|
+
setState((prev) => ({ ...prev, isInitialized: true }));
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
init();
|
|
66
|
+
|
|
67
|
+
// Listen for global onboarding completion events (e.g. from standalone onboarding components)
|
|
68
|
+
const subscription = DeviceEventEmitter.addListener(
|
|
69
|
+
"onboarding-complete",
|
|
70
|
+
() => {
|
|
71
|
+
setState((prev) => ({ ...prev, isOnboardingComplete: true }));
|
|
72
|
+
AsyncStorage.setItem(ONBOARDING_KEY, "true");
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return () => subscription.remove();
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
const completeOnboarding = useCallback(async () => {
|
|
80
|
+
try {
|
|
81
|
+
await AsyncStorage.setItem(ONBOARDING_KEY, "true");
|
|
82
|
+
setState((prev) => ({
|
|
83
|
+
...prev,
|
|
84
|
+
isOnboardingComplete: true,
|
|
85
|
+
showPostOnboardingPaywall: true,
|
|
86
|
+
}));
|
|
87
|
+
DeviceEventEmitter.emit("onboarding-complete");
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.error("[useSubscriptionFlow] Failed to complete onboarding:", e);
|
|
90
|
+
}
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const closePostOnboardingPaywall = useCallback(async () => {
|
|
94
|
+
try {
|
|
95
|
+
await AsyncStorage.setItem(PAYWALL_SHOWN_KEY, "true");
|
|
96
|
+
setState((prev) => ({
|
|
97
|
+
...prev,
|
|
98
|
+
showPostOnboardingPaywall: false,
|
|
99
|
+
paywallShown: true,
|
|
100
|
+
}));
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error("[useSubscriptionFlow] Failed to close paywall:", e);
|
|
103
|
+
}
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
const closeFeedback = useCallback(() => {
|
|
107
|
+
setState((prev) => ({ ...prev, showFeedback: false }));
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
const setAuthModalOpen = useCallback((open: boolean) => {
|
|
111
|
+
setState((prev) => ({ ...prev, isAuthModalOpen: open }));
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
const setShowFeedback = useCallback((show: boolean) => {
|
|
115
|
+
setState((prev) => ({ ...prev, showFeedback: show }));
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
const markPaywallShown = useCallback(async () => {
|
|
119
|
+
try {
|
|
120
|
+
await AsyncStorage.setItem(PAYWALL_SHOWN_KEY, "true");
|
|
121
|
+
setState((prev) => ({ ...prev, paywallShown: true }));
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.error("[useSubscriptionFlow] Failed to mark paywall shown:", e);
|
|
124
|
+
}
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
const resetFlow = useCallback(async () => {
|
|
128
|
+
try {
|
|
129
|
+
await Promise.all([
|
|
130
|
+
AsyncStorage.removeItem(ONBOARDING_KEY),
|
|
131
|
+
AsyncStorage.removeItem(PAYWALL_SHOWN_KEY),
|
|
132
|
+
]);
|
|
133
|
+
setState({
|
|
134
|
+
isInitialized: true,
|
|
135
|
+
isOnboardingComplete: false,
|
|
136
|
+
showPostOnboardingPaywall: false,
|
|
137
|
+
showFeedback: false,
|
|
138
|
+
paywallShown: false,
|
|
139
|
+
isAuthModalOpen: false,
|
|
140
|
+
});
|
|
141
|
+
} catch (e) {
|
|
142
|
+
console.error("[useSubscriptionFlow] Failed to reset flow:", e);
|
|
143
|
+
}
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
state,
|
|
148
|
+
completeOnboarding,
|
|
149
|
+
closePostOnboardingPaywall,
|
|
150
|
+
closeFeedback,
|
|
151
|
+
setAuthModalOpen,
|
|
152
|
+
markPaywallShown,
|
|
153
|
+
setShowFeedback,
|
|
154
|
+
resetFlow,
|
|
155
|
+
};
|
|
156
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -45,6 +45,8 @@ export { useDeductCredit } from "./domains/credits/presentation/deduct-credit/us
|
|
|
45
45
|
export { useFeatureGate } from "./domains/subscription/presentation/useFeatureGate";
|
|
46
46
|
export { usePaywallVisibility, paywallControl } from "./domains/subscription/presentation/usePaywallVisibility";
|
|
47
47
|
export { usePremium } from "./domains/subscription/presentation/usePremium";
|
|
48
|
+
export { useSubscriptionFlow } from "./domains/subscription/presentation/useSubscriptionFlow";
|
|
49
|
+
export type { SubscriptionFlowState, UseSubscriptionFlowResult } from "./domains/subscription/presentation/useSubscriptionFlow";
|
|
48
50
|
export { useSubscriptionStatus } from "./domains/subscription/presentation/useSubscriptionStatus";
|
|
49
51
|
export * from "./domains/subscription/presentation/useSubscriptionStatus.types";
|
|
50
52
|
export * from "./presentation/hooks/feedback/usePaywallFeedback";
|
|
@@ -65,6 +67,8 @@ export type {
|
|
|
65
67
|
} from "./domains/subscription/presentation/screens/SubscriptionDetailScreen.types";
|
|
66
68
|
export { PaywallScreen } from "./domains/paywall/components/PaywallScreen";
|
|
67
69
|
export type { PaywallScreenProps } from "./domains/paywall/components/PaywallScreen.types";
|
|
70
|
+
export { usePaywallOrchestrator } from "./domains/paywall/hooks/usePaywallOrchestrator";
|
|
71
|
+
export type { PaywallOrchestratorOptions } from "./domains/paywall/hooks/usePaywallOrchestrator";
|
|
68
72
|
|
|
69
73
|
export type {
|
|
70
74
|
CreditType,
|
|
@@ -86,8 +90,9 @@ export { toDate, toISOString, toTimestamp, getCurrentISOString } from "./shared/
|
|
|
86
90
|
// Credits Query Keys
|
|
87
91
|
export { creditsQueryKeys } from "./domains/credits/presentation/creditsQueryKeys";
|
|
88
92
|
|
|
89
|
-
// Paywall Types
|
|
90
|
-
export type { PaywallTranslations, PaywallLegalUrls } from "./domains/paywall/entities/types";
|
|
93
|
+
// Paywall Types & Utils
|
|
94
|
+
export type { PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "./domains/paywall/entities/types";
|
|
95
|
+
export { createPaywallTranslations } from "./domains/paywall/utils/paywallTranslationUtils";
|
|
91
96
|
|
|
92
97
|
// Purchase Loading Overlay
|
|
93
98
|
export { PurchaseLoadingOverlay } from "./domains/subscription/presentation/components/overlay/PurchaseLoadingOverlay";
|