@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.13",
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";