@umituz/react-native-subscription 2.43.1 → 2.43.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 +0 -1
- package/src/domains/paywall/hooks/usePaywallOrchestrator.ts +0 -4
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +2 -2
- package/src/domains/subscription/core/events/FlowEvents.ts +24 -0
- package/src/domains/subscription/core/events/SubscriptionEvents.ts +22 -0
- package/src/domains/subscription/infrastructure/hooks/usePurchasePackage.ts +2 -14
- package/src/domains/subscription/infrastructure/hooks/useRestorePurchase.ts +2 -14
- package/src/domains/subscription/presentation/featureGateActions.ts +27 -24
- package/src/domains/subscription/presentation/hooks/useFeatureGateState.ts +76 -0
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +11 -17
- package/src/domains/subscription/presentation/useFeatureGate.ts +32 -48
- package/src/domains/subscription/presentation/usePremium.ts +1 -6
- package/src/domains/subscription/presentation/utils/authCheckUtils.ts +42 -0
- package/src/index.ts +15 -4
- package/src/shared/infrastructure/SubscriptionEventBus.ts +6 -18
- package/src/shared/infrastructure/react-query/queryConfig.ts +0 -3
- package/src/shared/infrastructure/react-query/utils/cacheInvalidation.ts +97 -0
- package/src/shared/infrastructure/react-query/utils/index.ts +6 -0
- package/src/domains/subscription/presentation/components/overlay/PurchaseLoadingOverlay.tsx +0 -60
- package/src/domains/subscription/presentation/components/overlay/PurchaseLoadingOverlay.types.ts +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.43.
|
|
3
|
+
"version": "2.43.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",
|
|
@@ -22,9 +22,6 @@ export interface PaywallOrchestratorOptions {
|
|
|
22
22
|
* High-level orchestrator for Paywall navigation.
|
|
23
23
|
* Handles automatic triggers (post-onboarding) and manual triggers (showPaywall state).
|
|
24
24
|
* Centralizes handlers for success, close, and feedback triggers.
|
|
25
|
-
*
|
|
26
|
-
* This orchestrator fetches all subscription data and passes it to PaywallScreen as props.
|
|
27
|
-
* PaywallScreen is now a "dumb" component that doesn't call usePremium internally.
|
|
28
25
|
*/
|
|
29
26
|
export function usePaywallOrchestrator({
|
|
30
27
|
navigation,
|
|
@@ -86,7 +83,6 @@ export function usePaywallOrchestrator({
|
|
|
86
83
|
packagesCount: packages.length
|
|
87
84
|
});
|
|
88
85
|
|
|
89
|
-
// Pass all data and actions as props - PaywallScreen is now a dumb component
|
|
90
86
|
navigation.navigate("PaywallScreen", {
|
|
91
87
|
// UI Props
|
|
92
88
|
translations,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { PURCHASE_SOURCE, PURCHASE_TYPE } from "../core/SubscriptionConstants";
|
|
2
|
-
import
|
|
2
|
+
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
3
|
+
import type { PurchaseCompletedEvent, RenewalDetectedEvent, PremiumStatusChangedEvent } from "../core/SubscriptionEvents";
|
|
3
4
|
import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
|
|
4
5
|
import { extractRevenueCatData } from "./SubscriptionSyncUtils";
|
|
5
6
|
import { generatePurchaseId, generateRenewalId } from "./syncIdGenerators";
|
|
6
|
-
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Central processor for all subscription sync operations.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application flow events
|
|
3
|
+
* Events emitted during high-level application flow transitions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const FLOW_EVENTS = {
|
|
7
|
+
ONBOARDING_COMPLETED: "flow_onboarding_completed",
|
|
8
|
+
PAYWALL_SHOWN: "flow_paywall_shown",
|
|
9
|
+
PAYWALL_CLOSED: "flow_paywall_closed",
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export type FlowEventType = typeof FLOW_EVENTS[keyof typeof FLOW_EVENTS];
|
|
13
|
+
|
|
14
|
+
export interface OnboardingCompletedEvent {
|
|
15
|
+
timestamp: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PaywallShownEvent {
|
|
19
|
+
timestamp: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PaywallClosedEvent {
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription-related events
|
|
3
|
+
* Events emitted during subscription lifecycle operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const SUBSCRIPTION_EVENTS = {
|
|
7
|
+
CREDITS_UPDATED: "credits_updated",
|
|
8
|
+
PURCHASE_COMPLETED: "purchase_completed",
|
|
9
|
+
RENEWAL_DETECTED: "renewal_detected",
|
|
10
|
+
PREMIUM_STATUS_CHANGED: "premium_status_changed",
|
|
11
|
+
SYNC_STATUS_CHANGED: "sync_status_changed",
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export type SubscriptionEventType = typeof SUBSCRIPTION_EVENTS[keyof typeof SUBSCRIPTION_EVENTS];
|
|
15
|
+
|
|
16
|
+
export interface SyncStatusChangedEvent {
|
|
17
|
+
status: 'syncing' | 'success' | 'error';
|
|
18
|
+
phase: 'purchase' | 'renewal';
|
|
19
|
+
userId?: string;
|
|
20
|
+
productId?: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
@@ -6,10 +6,8 @@ import {
|
|
|
6
6
|
selectUserId,
|
|
7
7
|
} from "@umituz/react-native-auth";
|
|
8
8
|
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
9
|
-
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
10
|
-
import { subscriptionStatusQueryKeys } from "../../presentation/useSubscriptionStatus";
|
|
11
|
-
import { creditsQueryKeys } from "../../../credits/presentation/creditsQueryKeys";
|
|
12
9
|
import { getErrorMessage } from "../../../revenuecat/core/errors/RevenueCatErrorHandler";
|
|
10
|
+
import { invalidateSubscriptionCaches } from "../../../../shared/infrastructure/react-query/utils";
|
|
13
11
|
|
|
14
12
|
interface PurchaseMutationResult {
|
|
15
13
|
success: boolean;
|
|
@@ -39,17 +37,7 @@ export const usePurchasePackage = () => {
|
|
|
39
37
|
onSuccess: (result) => {
|
|
40
38
|
if (result.success) {
|
|
41
39
|
showSuccess("Purchase Successful", "Your subscription is now active!");
|
|
42
|
-
queryClient
|
|
43
|
-
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
44
|
-
});
|
|
45
|
-
if (userId) {
|
|
46
|
-
queryClient.invalidateQueries({
|
|
47
|
-
queryKey: subscriptionStatusQueryKeys.user(userId),
|
|
48
|
-
});
|
|
49
|
-
queryClient.invalidateQueries({
|
|
50
|
-
queryKey: creditsQueryKeys.user(userId),
|
|
51
|
-
});
|
|
52
|
-
}
|
|
40
|
+
invalidateSubscriptionCaches(queryClient, userId);
|
|
53
41
|
} else {
|
|
54
42
|
showError("Purchase Failed", "Unable to complete purchase. Please try again.");
|
|
55
43
|
}
|
|
@@ -5,10 +5,8 @@ import {
|
|
|
5
5
|
selectUserId,
|
|
6
6
|
} from "@umituz/react-native-auth";
|
|
7
7
|
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
8
|
-
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
9
|
-
import { subscriptionStatusQueryKeys } from "../../presentation/useSubscriptionStatus";
|
|
10
|
-
import { creditsQueryKeys } from "../../../credits/presentation/creditsQueryKeys";
|
|
11
8
|
import { getErrorMessage } from "../../../revenuecat/core/errors/RevenueCatErrorHandler";
|
|
9
|
+
import { invalidateSubscriptionCaches } from "../../../../shared/infrastructure/react-query/utils";
|
|
12
10
|
|
|
13
11
|
interface RestoreResult {
|
|
14
12
|
success: boolean;
|
|
@@ -31,17 +29,7 @@ export const useRestorePurchase = () => {
|
|
|
31
29
|
},
|
|
32
30
|
onSuccess: (result) => {
|
|
33
31
|
if (result.success) {
|
|
34
|
-
queryClient
|
|
35
|
-
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
36
|
-
});
|
|
37
|
-
if (userId) {
|
|
38
|
-
queryClient.invalidateQueries({
|
|
39
|
-
queryKey: subscriptionStatusQueryKeys.user(userId),
|
|
40
|
-
});
|
|
41
|
-
queryClient.invalidateQueries({
|
|
42
|
-
queryKey: creditsQueryKeys.user(userId),
|
|
43
|
-
});
|
|
44
|
-
}
|
|
32
|
+
invalidateSubscriptionCaches(queryClient, userId);
|
|
45
33
|
|
|
46
34
|
if (result.productId) {
|
|
47
35
|
showSuccess("Restore Successful", "Your subscription has been restored!");
|
|
@@ -1,50 +1,53 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FeatureGateState } from "./hooks/useFeatureGateState";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Executes a feature action with proper auth, subscription, and credit checks.
|
|
5
|
+
* Queues the action if waiting for auth completion or purchase flow.
|
|
6
|
+
*
|
|
7
|
+
* @param action - The action to execute when conditions are met
|
|
8
|
+
* @param isAuthenticated - Whether user is authenticated
|
|
9
|
+
* @param onShowAuthModal - Callback to show auth modal
|
|
10
|
+
* @param state - Feature gate state containing all refs and flags
|
|
11
|
+
* @returns true if action was executed immediately, false if queued/pending
|
|
12
|
+
*/
|
|
13
|
+
export function executeFeatureAction(
|
|
4
14
|
action: () => void | Promise<void>,
|
|
5
15
|
isAuthenticated: boolean,
|
|
6
16
|
onShowAuthModal: (callback: () => void | Promise<void>) => void,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
requiredCreditsRef: MutableRefObject<number>,
|
|
10
|
-
onShowPaywallRef: MutableRefObject<(requiredCredits?: number) => void>,
|
|
11
|
-
pendingActionRef: MutableRefObject<(() => void | Promise<void>) | null>,
|
|
12
|
-
isWaitingForAuthCreditsRef: MutableRefObject<boolean>,
|
|
13
|
-
isWaitingForPurchaseRef: MutableRefObject<boolean>,
|
|
14
|
-
isCreditsLoadedRef: MutableRefObject<boolean>
|
|
15
|
-
): boolean => {
|
|
17
|
+
state: FeatureGateState
|
|
18
|
+
): boolean {
|
|
16
19
|
|
|
17
20
|
if (!isAuthenticated) {
|
|
18
21
|
const postAuthAction = () => {
|
|
19
|
-
if (hasSubscriptionRef.current || creditBalanceRef.current >= requiredCreditsRef.current) {
|
|
22
|
+
if (state.hasSubscriptionRef.current || state.creditBalanceRef.current >= state.requiredCreditsRef.current) {
|
|
20
23
|
action();
|
|
21
24
|
return;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
if (isCreditsLoadedRef.current) {
|
|
25
|
-
pendingActionRef.current = action;
|
|
26
|
-
isWaitingForPurchaseRef.current = true;
|
|
27
|
-
onShowPaywallRef.current(requiredCreditsRef.current);
|
|
27
|
+
if (state.isCreditsLoadedRef.current) {
|
|
28
|
+
state.pendingActionRef.current = action;
|
|
29
|
+
state.isWaitingForPurchaseRef.current = true;
|
|
30
|
+
state.onShowPaywallRef.current(state.requiredCreditsRef.current);
|
|
28
31
|
return;
|
|
29
32
|
}
|
|
30
|
-
pendingActionRef.current = action;
|
|
31
|
-
isWaitingForAuthCreditsRef.current = true;
|
|
33
|
+
state.pendingActionRef.current = action;
|
|
34
|
+
state.isWaitingForAuthCreditsRef.current = true;
|
|
32
35
|
};
|
|
33
36
|
onShowAuthModal(postAuthAction);
|
|
34
37
|
return false;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
if (hasSubscriptionRef.current) {
|
|
40
|
+
if (state.hasSubscriptionRef.current) {
|
|
38
41
|
action();
|
|
39
42
|
return true;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
if (creditBalanceRef.current < requiredCreditsRef.current) {
|
|
43
|
-
pendingActionRef.current = action;
|
|
44
|
-
isWaitingForPurchaseRef.current = true;
|
|
45
|
-
onShowPaywallRef.current(requiredCreditsRef.current);
|
|
45
|
+
if (state.creditBalanceRef.current < state.requiredCreditsRef.current) {
|
|
46
|
+
state.pendingActionRef.current = action;
|
|
47
|
+
state.isWaitingForPurchaseRef.current = true;
|
|
48
|
+
state.onShowPaywallRef.current(state.requiredCreditsRef.current);
|
|
46
49
|
return false;
|
|
47
50
|
}
|
|
48
51
|
action();
|
|
49
52
|
return true;
|
|
50
|
-
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import type { UseFeatureGateParams } from "../useFeatureGate.types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Internal state management for useFeatureGate hook.
|
|
6
|
+
* Encapsulates all refs and state to reduce parameter passing.
|
|
7
|
+
*/
|
|
8
|
+
export interface FeatureGateState {
|
|
9
|
+
// Action queue
|
|
10
|
+
pendingActionRef: React.MutableRefObject<(() => void | Promise<void>) | null>;
|
|
11
|
+
|
|
12
|
+
// Previous values for change detection
|
|
13
|
+
prevCreditBalanceRef: React.MutableRefObject<number | undefined>;
|
|
14
|
+
prevHasSubscriptionRef: React.MutableRefObject<boolean>;
|
|
15
|
+
|
|
16
|
+
// Waiting flags for async operations
|
|
17
|
+
isWaitingForAuthCreditsRef: React.MutableRefObject<boolean>;
|
|
18
|
+
isWaitingForPurchaseRef: React.MutableRefObject<boolean>;
|
|
19
|
+
|
|
20
|
+
// Live refs (synced with current values)
|
|
21
|
+
creditBalanceRef: React.MutableRefObject<number>;
|
|
22
|
+
hasSubscriptionRef: React.MutableRefObject<boolean>;
|
|
23
|
+
onShowPaywallRef: React.MutableRefObject<(requiredCredits?: number) => void>;
|
|
24
|
+
requiredCreditsRef: React.MutableRefObject<number>;
|
|
25
|
+
isCreditsLoadedRef: React.MutableRefObject<boolean>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates and initializes all refs for useFeatureGate.
|
|
30
|
+
* This encapsulates ref creation and initialization logic.
|
|
31
|
+
*/
|
|
32
|
+
export function useFeatureGateRefs(params: UseFeatureGateParams): FeatureGateState {
|
|
33
|
+
const {
|
|
34
|
+
creditBalance,
|
|
35
|
+
hasSubscription = false,
|
|
36
|
+
onShowPaywall,
|
|
37
|
+
requiredCredits = 1,
|
|
38
|
+
isCreditsLoaded = true,
|
|
39
|
+
} = params;
|
|
40
|
+
|
|
41
|
+
const pendingActionRef = useRef<(() => void | Promise<void>) | null>(null);
|
|
42
|
+
const prevCreditBalanceRef = useRef(creditBalance);
|
|
43
|
+
const prevHasSubscriptionRef = useRef(hasSubscription);
|
|
44
|
+
const isWaitingForAuthCreditsRef = useRef(false);
|
|
45
|
+
const isWaitingForPurchaseRef = useRef(false);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
pendingActionRef,
|
|
49
|
+
prevCreditBalanceRef,
|
|
50
|
+
prevHasSubscriptionRef,
|
|
51
|
+
isWaitingForAuthCreditsRef,
|
|
52
|
+
isWaitingForPurchaseRef,
|
|
53
|
+
creditBalanceRef: useRef(creditBalance),
|
|
54
|
+
hasSubscriptionRef: useRef(hasSubscription),
|
|
55
|
+
onShowPaywallRef: useRef(onShowPaywall),
|
|
56
|
+
requiredCreditsRef: useRef(requiredCredits),
|
|
57
|
+
isCreditsLoadedRef: useRef(isCreditsLoaded),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Updates live refs when their source values change.
|
|
63
|
+
* Call this in a useEffect or when values update.
|
|
64
|
+
*/
|
|
65
|
+
export function updateLiveRefs(
|
|
66
|
+
state: FeatureGateState,
|
|
67
|
+
params: Pick<UseFeatureGateParams, 'creditBalance' | 'hasSubscription' | 'onShowPaywall' | 'requiredCredits' | 'isCreditsLoaded'>
|
|
68
|
+
): void {
|
|
69
|
+
const { creditBalance, hasSubscription, onShowPaywall, requiredCredits, isCreditsLoaded } = params;
|
|
70
|
+
|
|
71
|
+
state.creditBalanceRef.current = creditBalance;
|
|
72
|
+
state.hasSubscriptionRef.current = hasSubscription;
|
|
73
|
+
state.onShowPaywallRef.current = onShowPaywall;
|
|
74
|
+
state.requiredCreditsRef.current = requiredCredits;
|
|
75
|
+
state.isCreditsLoadedRef.current = isCreditsLoaded;
|
|
76
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { PurchasesPackage } from "react-native-purchases";
|
|
|
3
3
|
import { usePremium } from "./usePremium";
|
|
4
4
|
import type { PurchaseSource } from "../core/SubscriptionConstants";
|
|
5
5
|
import { authPurchaseStateManager } from "../infrastructure/utils/authPurchaseState";
|
|
6
|
+
import { requireAuthentication } from "./utils/authCheckUtils";
|
|
6
7
|
|
|
7
8
|
export const configureAuthProvider = (provider: import("../infrastructure/utils/authPurchaseState").PurchaseAuthProvider): void => {
|
|
8
9
|
authPurchaseStateManager.configure(provider);
|
|
@@ -26,6 +27,10 @@ interface UseAuthAwarePurchaseResult {
|
|
|
26
27
|
executeSavedPurchase: () => Promise<boolean>;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Hook for purchase operations that handle authentication.
|
|
32
|
+
* Automatically saves pending purchases and shows auth modal when needed.
|
|
33
|
+
*/
|
|
29
34
|
export const useAuthAwarePurchase = (
|
|
30
35
|
params?: UseAuthAwarePurchaseParams
|
|
31
36
|
): UseAuthAwarePurchaseResult => {
|
|
@@ -50,11 +55,10 @@ export const useAuthAwarePurchase = (
|
|
|
50
55
|
}
|
|
51
56
|
}, [purchasePackage]);
|
|
52
57
|
|
|
58
|
+
// Auto-execute saved purchase when user authenticates
|
|
53
59
|
useEffect(() => {
|
|
54
60
|
const authProvider = authPurchaseStateManager.getProvider();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const hasUser = authProvider.hasFirebaseUser();
|
|
61
|
+
const hasUser = authProvider && authProvider.hasFirebaseUser();
|
|
58
62
|
const hasSavedPurchase = !!authPurchaseStateManager.getSavedPurchase();
|
|
59
63
|
|
|
60
64
|
if (hasUser && hasSavedPurchase && !isExecutingSavedRef.current) {
|
|
@@ -69,18 +73,14 @@ export const useAuthAwarePurchase = (
|
|
|
69
73
|
async (pkg: PurchasesPackage, source?: PurchaseSource): Promise<boolean> => {
|
|
70
74
|
const authProvider = authPurchaseStateManager.getProvider();
|
|
71
75
|
|
|
72
|
-
if (!authProvider) {
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!authProvider.hasFirebaseUser()) {
|
|
76
|
+
if (!requireAuthentication(authProvider)) {
|
|
77
|
+
// User not authenticated, purchase saved and auth modal shown
|
|
77
78
|
authPurchaseStateManager.savePurchase(pkg, source || params?.source || "settings");
|
|
78
|
-
authProvider.showAuthModal();
|
|
79
79
|
return false;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// User authenticated, proceed with purchase
|
|
82
83
|
const result = await purchasePackage(pkg);
|
|
83
|
-
|
|
84
84
|
return result;
|
|
85
85
|
},
|
|
86
86
|
[purchasePackage, params?.source]
|
|
@@ -89,17 +89,11 @@ export const useAuthAwarePurchase = (
|
|
|
89
89
|
const handleRestore = useCallback(async (): Promise<boolean> => {
|
|
90
90
|
const authProvider = authPurchaseStateManager.getProvider();
|
|
91
91
|
|
|
92
|
-
if (!authProvider) {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!authProvider.hasFirebaseUser()) {
|
|
97
|
-
authProvider.showAuthModal();
|
|
92
|
+
if (!requireAuthentication(authProvider)) {
|
|
98
93
|
return false;
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
const result = await restorePurchase();
|
|
102
|
-
|
|
103
97
|
return result;
|
|
104
98
|
}, [restorePurchase]);
|
|
105
99
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useCallback,
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
|
2
2
|
import type { UseFeatureGateParams, UseFeatureGateResult } from "./useFeatureGate.types";
|
|
3
3
|
import { DEFAULT_REQUIRED_CREDITS, canExecuteAuthAction, canExecutePurchaseAction } from "../application/featureGate/featureGateBusinessRules";
|
|
4
|
-
import { useSyncedRefs } from "./featureGateRefs";
|
|
5
4
|
import { executeFeatureAction } from "./featureGateActions";
|
|
5
|
+
import { useFeatureGateRefs, updateLiveRefs } from "./hooks/useFeatureGateState";
|
|
6
6
|
|
|
7
7
|
export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResult {
|
|
8
8
|
const {
|
|
@@ -11,73 +11,64 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
11
11
|
hasSubscription = false,
|
|
12
12
|
creditBalance,
|
|
13
13
|
requiredCredits = DEFAULT_REQUIRED_CREDITS,
|
|
14
|
-
onShowPaywall,
|
|
15
14
|
isCreditsLoaded = true,
|
|
16
15
|
} = params;
|
|
17
16
|
|
|
18
|
-
const
|
|
19
|
-
const prevCreditBalanceRef = useRef(creditBalance);
|
|
20
|
-
// Separate ref to track previous subscription state for canExecutePurchaseAction.
|
|
21
|
-
// NOTE: Must NOT use hasSubscriptionRef from useSyncedRefs here because useSyncedRefs
|
|
22
|
-
// effects run BEFORE this effect (React runs effects in definition order), so
|
|
23
|
-
// hasSubscriptionRef.current would already be the NEW value when we check it.
|
|
24
|
-
const prevHasSubscriptionRef = useRef(hasSubscription);
|
|
25
|
-
const isWaitingForPurchaseRef = useRef(false);
|
|
26
|
-
const isWaitingForAuthCreditsRef = useRef(false);
|
|
17
|
+
const state = useFeatureGateRefs(params);
|
|
27
18
|
|
|
28
|
-
|
|
19
|
+
// Update live refs when params change
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
updateLiveRefs(state, params);
|
|
22
|
+
});
|
|
29
23
|
|
|
24
|
+
// Handle post-auth credit loading and action execution
|
|
30
25
|
useEffect(() => {
|
|
31
26
|
const shouldExecute = canExecuteAuthAction(
|
|
32
|
-
isWaitingForAuthCreditsRef.current,
|
|
27
|
+
state.isWaitingForAuthCreditsRef.current,
|
|
33
28
|
isCreditsLoaded,
|
|
34
|
-
!!pendingActionRef.current,
|
|
29
|
+
!!state.pendingActionRef.current,
|
|
35
30
|
hasSubscription,
|
|
36
31
|
creditBalance,
|
|
37
32
|
requiredCredits
|
|
38
33
|
);
|
|
39
34
|
|
|
40
35
|
if (shouldExecute) {
|
|
41
|
-
isWaitingForAuthCreditsRef.current = false;
|
|
42
|
-
const action = pendingActionRef.current!;
|
|
43
|
-
pendingActionRef.current = null;
|
|
36
|
+
state.isWaitingForAuthCreditsRef.current = false;
|
|
37
|
+
const action = state.pendingActionRef.current!;
|
|
38
|
+
state.pendingActionRef.current = null;
|
|
44
39
|
action();
|
|
45
40
|
return;
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
if (isWaitingForAuthCreditsRef.current && isCreditsLoaded && pendingActionRef.current) {
|
|
49
|
-
isWaitingForAuthCreditsRef.current = false;
|
|
50
|
-
isWaitingForPurchaseRef.current = true;
|
|
51
|
-
|
|
52
|
-
onShowPaywallRef.current(requiredCreditsRef.current);
|
|
43
|
+
if (state.isWaitingForAuthCreditsRef.current && isCreditsLoaded && state.pendingActionRef.current) {
|
|
44
|
+
state.isWaitingForAuthCreditsRef.current = false;
|
|
45
|
+
state.isWaitingForPurchaseRef.current = true;
|
|
46
|
+
state.onShowPaywallRef.current(state.requiredCreditsRef.current);
|
|
53
47
|
}
|
|
54
|
-
|
|
55
|
-
}, [isCreditsLoaded, creditBalance, hasSubscription, requiredCredits, onShowPaywallRef, requiredCreditsRef]);
|
|
48
|
+
}, [isCreditsLoaded, creditBalance, hasSubscription, requiredCredits, state]);
|
|
56
49
|
|
|
50
|
+
// Handle post-purchase action execution
|
|
57
51
|
useEffect(() => {
|
|
58
|
-
// Use prevHasSubscriptionRef (updated AFTER check) not hasSubscriptionRef from useSyncedRefs
|
|
59
|
-
// (which is already updated to new value before this effect runs - race condition fix)
|
|
60
52
|
const shouldExecute = canExecutePurchaseAction(
|
|
61
|
-
isWaitingForPurchaseRef.current,
|
|
53
|
+
state.isWaitingForPurchaseRef.current,
|
|
62
54
|
creditBalance,
|
|
63
|
-
prevCreditBalanceRef.current ?? 0,
|
|
55
|
+
state.prevCreditBalanceRef.current ?? 0,
|
|
64
56
|
hasSubscription,
|
|
65
|
-
prevHasSubscriptionRef.current,
|
|
66
|
-
!!pendingActionRef.current
|
|
57
|
+
state.prevHasSubscriptionRef.current,
|
|
58
|
+
!!state.pendingActionRef.current
|
|
67
59
|
);
|
|
68
60
|
|
|
69
61
|
if (shouldExecute) {
|
|
70
|
-
const action = pendingActionRef.current!;
|
|
71
|
-
pendingActionRef.current = null;
|
|
72
|
-
isWaitingForPurchaseRef.current = false;
|
|
62
|
+
const action = state.pendingActionRef.current!;
|
|
63
|
+
state.pendingActionRef.current = null;
|
|
64
|
+
state.isWaitingForPurchaseRef.current = false;
|
|
73
65
|
action();
|
|
74
66
|
}
|
|
75
67
|
|
|
76
|
-
// Update AFTER check
|
|
77
|
-
prevCreditBalanceRef.current = creditBalance;
|
|
78
|
-
prevHasSubscriptionRef.current = hasSubscription;
|
|
79
|
-
|
|
80
|
-
}, [creditBalance, hasSubscription]);
|
|
68
|
+
// Update prev refs AFTER check for next render
|
|
69
|
+
state.prevCreditBalanceRef.current = creditBalance;
|
|
70
|
+
state.prevHasSubscriptionRef.current = hasSubscription;
|
|
71
|
+
}, [creditBalance, hasSubscription, state]);
|
|
81
72
|
|
|
82
73
|
const requireFeature = useCallback(
|
|
83
74
|
(action: () => void | Promise<void>): boolean => {
|
|
@@ -85,17 +76,10 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
85
76
|
action,
|
|
86
77
|
isAuthenticated,
|
|
87
78
|
onShowAuthModal,
|
|
88
|
-
|
|
89
|
-
creditBalanceRef,
|
|
90
|
-
requiredCreditsRef,
|
|
91
|
-
onShowPaywallRef,
|
|
92
|
-
pendingActionRef,
|
|
93
|
-
isWaitingForAuthCreditsRef,
|
|
94
|
-
isWaitingForPurchaseRef,
|
|
95
|
-
isCreditsLoadedRef
|
|
79
|
+
state
|
|
96
80
|
);
|
|
97
81
|
},
|
|
98
|
-
[isAuthenticated, onShowAuthModal,
|
|
82
|
+
[isAuthenticated, onShowAuthModal, state]
|
|
99
83
|
);
|
|
100
84
|
|
|
101
85
|
return {
|
|
@@ -7,11 +7,7 @@ import { UsePremiumResult } from './usePremium.types';
|
|
|
7
7
|
/**
|
|
8
8
|
* Facade hook that combines status, packages, and actions.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* components to use more focused hooks (usePremiumStatus, usePremiumPackages, usePremiumActions)
|
|
12
|
-
* for better performance and testability.
|
|
13
|
-
*
|
|
14
|
-
* For new components, consider using the focused hooks:
|
|
10
|
+
* Consider using the focused hooks for better performance:
|
|
15
11
|
* - usePremiumStatus() - when you only need premium status
|
|
16
12
|
* - usePremiumPackages() - when you only need package data
|
|
17
13
|
* - usePremiumActions() - when you only need actions
|
|
@@ -28,7 +24,6 @@ export const usePremium = (): UsePremiumResult => {
|
|
|
28
24
|
...status,
|
|
29
25
|
...packages,
|
|
30
26
|
...actions,
|
|
31
|
-
// Merge loading states for backward compatibility
|
|
32
27
|
isLoading: status.isSyncing || packages.isLoading || actions.isLoading,
|
|
33
28
|
}), [
|
|
34
29
|
status,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication check utilities for purchase flows.
|
|
3
|
+
* Extracted to reduce duplication in useAuthAwarePurchase.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface PurchaseAuthProvider {
|
|
7
|
+
hasFirebaseUser: () => boolean;
|
|
8
|
+
showAuthModal: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checks if auth provider is available and user is authenticated.
|
|
13
|
+
*
|
|
14
|
+
* @param authProvider - Auth provider from authPurchaseStateManager
|
|
15
|
+
* @returns true if user is authenticated, false otherwise
|
|
16
|
+
*/
|
|
17
|
+
export function isUserAuthenticated(authProvider: PurchaseAuthProvider | null): boolean {
|
|
18
|
+
if (!authProvider) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return authProvider.hasFirebaseUser();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ensures user is authenticated before proceeding with an action.
|
|
26
|
+
* If not authenticated, shows auth modal and returns false.
|
|
27
|
+
*
|
|
28
|
+
* @param authProvider - Auth provider from authPurchaseStateManager
|
|
29
|
+
* @returns true if authenticated, false if auth modal was shown
|
|
30
|
+
*/
|
|
31
|
+
export function requireAuthentication(authProvider: PurchaseAuthProvider | null): boolean {
|
|
32
|
+
if (!authProvider) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!authProvider.hasFirebaseUser()) {
|
|
37
|
+
authProvider.showAuthModal();
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return true;
|
|
42
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,13 @@ export {
|
|
|
9
9
|
PURCHASE_TYPE,
|
|
10
10
|
ANONYMOUS_CACHE_KEY,
|
|
11
11
|
} from "./domains/subscription/core/SubscriptionConstants";
|
|
12
|
+
|
|
13
|
+
// Domain Events
|
|
14
|
+
export { SUBSCRIPTION_EVENTS } from "./domains/subscription/core/events/SubscriptionEvents";
|
|
15
|
+
export type { SubscriptionEventType, SyncStatusChangedEvent } from "./domains/subscription/core/events/SubscriptionEvents";
|
|
16
|
+
export type { PurchaseCompletedEvent, RenewalDetectedEvent, PremiumStatusChangedEvent } from "./domains/subscription/core/SubscriptionEvents";
|
|
17
|
+
export { FLOW_EVENTS } from "./domains/subscription/core/events/FlowEvents";
|
|
18
|
+
export type { FlowEventType, OnboardingCompletedEvent, PaywallShownEvent, PaywallClosedEvent } from "./domains/subscription/core/events/FlowEvents";
|
|
12
19
|
export type {
|
|
13
20
|
UserTierType,
|
|
14
21
|
SubscriptionStatusType,
|
|
@@ -44,6 +51,14 @@ export {
|
|
|
44
51
|
} from "./shared/utils/Result";
|
|
45
52
|
export type { Result, Success, Failure } from "./shared/utils/Result";
|
|
46
53
|
|
|
54
|
+
// Cache Invalidation Utilities
|
|
55
|
+
export {
|
|
56
|
+
invalidateSubscriptionCaches,
|
|
57
|
+
invalidateSubscriptionStatus,
|
|
58
|
+
invalidateCredits,
|
|
59
|
+
invalidateAllUserData,
|
|
60
|
+
} from "./shared/infrastructure/react-query/utils";
|
|
61
|
+
|
|
47
62
|
// Infrastructure Layer (Services & Repositories)
|
|
48
63
|
export { initializeSubscription } from "./domains/subscription/application/initializer/SubscriptionInitializer";
|
|
49
64
|
export type { SubscriptionInitConfig, CreditPackageConfig } from "./domains/subscription/application/SubscriptionInitializerTypes";
|
|
@@ -176,10 +191,6 @@ export type { ManagedSubscriptionFlowProps } from "./domains/subscription/presen
|
|
|
176
191
|
export { SubscriptionFlowStatus } from "./domains/subscription/presentation/useSubscriptionFlow";
|
|
177
192
|
export { SubscriptionFlowProvider, useSubscriptionFlowStatus } from "./domains/subscription/presentation/providers/SubscriptionFlowProvider";
|
|
178
193
|
|
|
179
|
-
// Purchase Loading Overlay
|
|
180
|
-
export { PurchaseLoadingOverlay } from "./domains/subscription/presentation/components/overlay/PurchaseLoadingOverlay";
|
|
181
|
-
export type { PurchaseLoadingOverlayProps } from "./domains/subscription/presentation/components/overlay/PurchaseLoadingOverlay";
|
|
182
|
-
|
|
183
194
|
// Init Module Factory
|
|
184
195
|
export {
|
|
185
196
|
createSubscriptionInitModule,
|
|
@@ -17,7 +17,7 @@ class SubscriptionEventBus {
|
|
|
17
17
|
if (!this.listeners.has(event)) {
|
|
18
18
|
this.listeners.set(event, new Set());
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
const eventSet = this.listeners.get(event)!;
|
|
22
22
|
eventSet.add(callback as EventCallback);
|
|
23
23
|
|
|
@@ -36,8 +36,6 @@ class SubscriptionEventBus {
|
|
|
36
36
|
const listeners = this.listeners.get(event);
|
|
37
37
|
if (!listeners || listeners.size === 0) return;
|
|
38
38
|
|
|
39
|
-
// Use microtask for async execution to not block main thread
|
|
40
|
-
// but keep it fast.
|
|
41
39
|
listeners.forEach(callback => {
|
|
42
40
|
queueMicrotask(() => {
|
|
43
41
|
try {
|
|
@@ -71,19 +69,9 @@ class SubscriptionEventBus {
|
|
|
71
69
|
|
|
72
70
|
export const subscriptionEventBus = SubscriptionEventBus.getInstance();
|
|
73
71
|
|
|
74
|
-
export
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
SYNC_STATUS_CHANGED: "sync_status_changed",
|
|
80
|
-
} as const;
|
|
81
|
-
|
|
82
|
-
export const FLOW_EVENTS = {
|
|
83
|
-
ONBOARDING_COMPLETED: "flow_onboarding_completed",
|
|
84
|
-
PAYWALL_SHOWN: "flow_paywall_shown",
|
|
85
|
-
PAYWALL_CLOSED: "flow_paywall_closed",
|
|
86
|
-
} as const;
|
|
72
|
+
// Re-export event constants for external use
|
|
73
|
+
export { SUBSCRIPTION_EVENTS } from "../../domains/subscription/core/events/SubscriptionEvents";
|
|
74
|
+
export { FLOW_EVENTS } from "../../domains/subscription/core/events/FlowEvents";
|
|
75
|
+
export type { SubscriptionEventType } from "../../domains/subscription/core/events/SubscriptionEvents";
|
|
76
|
+
export type { FlowEventType } from "../../domains/subscription/core/events/FlowEvents";
|
|
87
77
|
|
|
88
|
-
export type SubscriptionEventType = typeof SUBSCRIPTION_EVENTS[keyof typeof SUBSCRIPTION_EVENTS];
|
|
89
|
-
export type FlowEventType = typeof FLOW_EVENTS[keyof typeof FLOW_EVENTS];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { SUBSCRIPTION_QUERY_KEYS } from "../../../../domains/subscription/infrastructure/hooks/subscriptionQueryKeys";
|
|
3
|
+
import { subscriptionStatusQueryKeys } from "../../../../domains/subscription/presentation/useSubscriptionStatus";
|
|
4
|
+
import { creditsQueryKeys } from "../../../../domains/credits/presentation/creditsQueryKeys";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Centralized cache invalidation utilities for subscription-related queries.
|
|
8
|
+
* This ensures consistent cache invalidation across all mutations and removes code duplication.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Invalidate all subscription-related caches for a specific user.
|
|
13
|
+
* This includes:
|
|
14
|
+
* - Subscription packages
|
|
15
|
+
* - Subscription status
|
|
16
|
+
* - Credits
|
|
17
|
+
*
|
|
18
|
+
* @param queryClient - TanStack Query client instance
|
|
19
|
+
* @param userId - User ID to invalidate caches for
|
|
20
|
+
*/
|
|
21
|
+
export function invalidateSubscriptionCaches(
|
|
22
|
+
queryClient: QueryClient,
|
|
23
|
+
userId: string | null | undefined
|
|
24
|
+
): void {
|
|
25
|
+
if (!userId) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Invalidate packages (global, not user-specific)
|
|
30
|
+
queryClient.invalidateQueries({
|
|
31
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Invalidate subscription status (user-specific)
|
|
35
|
+
queryClient.invalidateQueries({
|
|
36
|
+
queryKey: subscriptionStatusQueryKeys.user(userId),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Invalidate credits (user-specific)
|
|
40
|
+
queryClient.invalidateQueries({
|
|
41
|
+
queryKey: creditsQueryKeys.user(userId),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Invalidate only subscription status cache.
|
|
47
|
+
* Use this when only subscription status changes, not credits.
|
|
48
|
+
*
|
|
49
|
+
* @param queryClient - TanStack Query client instance
|
|
50
|
+
* @param userId - User ID to invalidate cache for
|
|
51
|
+
*/
|
|
52
|
+
export function invalidateSubscriptionStatus(
|
|
53
|
+
queryClient: QueryClient,
|
|
54
|
+
userId: string | null | undefined
|
|
55
|
+
): void {
|
|
56
|
+
if (!userId) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
queryClient.invalidateQueries({
|
|
61
|
+
queryKey: subscriptionStatusQueryKeys.user(userId),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Invalidate only credits cache.
|
|
67
|
+
* Use this when only credits change, not subscription status.
|
|
68
|
+
*
|
|
69
|
+
* @param queryClient - TanStack Query client instance
|
|
70
|
+
* @param userId - User ID to invalidate cache for
|
|
71
|
+
*/
|
|
72
|
+
export function invalidateCredits(
|
|
73
|
+
queryClient: QueryClient,
|
|
74
|
+
userId: string | null | undefined
|
|
75
|
+
): void {
|
|
76
|
+
if (!userId) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
queryClient.invalidateQueries({
|
|
81
|
+
queryKey: creditsQueryKeys.user(userId),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Invalidate all caches for a user (subscription + credits).
|
|
87
|
+
* Alias for invalidateSubscriptionCaches for better semantic clarity.
|
|
88
|
+
*
|
|
89
|
+
* @param queryClient - TanStack Query client instance
|
|
90
|
+
* @param userId - User ID to invalidate caches for
|
|
91
|
+
*/
|
|
92
|
+
export function invalidateAllUserData(
|
|
93
|
+
queryClient: QueryClient,
|
|
94
|
+
userId: string | null | undefined
|
|
95
|
+
): void {
|
|
96
|
+
invalidateSubscriptionCaches(queryClient, userId);
|
|
97
|
+
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purchase Loading Overlay
|
|
3
|
-
* Full-screen overlay shown during purchase operations
|
|
4
|
-
* Locks the UI and shows a spinner with optional message
|
|
5
|
-
*
|
|
6
|
-
* This is now a props-based component. Pass isLoading from parent component.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import React from "react";
|
|
10
|
-
import { View, Modal, StyleSheet } from "react-native";
|
|
11
|
-
import { AtomicSpinner, AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
12
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
13
|
-
import type { PurchaseLoadingOverlayProps } from "./PurchaseLoadingOverlay.types";
|
|
14
|
-
|
|
15
|
-
export type { PurchaseLoadingOverlayProps };
|
|
16
|
-
|
|
17
|
-
export const PurchaseLoadingOverlay: React.FC<PurchaseLoadingOverlayProps> = React.memo(
|
|
18
|
-
({ loadingText, isLoading }) => {
|
|
19
|
-
const tokens = useAppDesignTokens();
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<Modal visible={isLoading} transparent animationType="none" statusBarTranslucent>
|
|
23
|
-
<View style={[styles.container, { backgroundColor: "rgba(0, 0, 0, 0.7)" }]}>
|
|
24
|
-
<View style={[styles.content, { backgroundColor: tokens.colors.surface }]}>
|
|
25
|
-
<AtomicSpinner size="lg" color="primary" />
|
|
26
|
-
{loadingText && (
|
|
27
|
-
<AtomicText
|
|
28
|
-
type="bodyLarge"
|
|
29
|
-
style={[styles.text, { color: tokens.colors.textPrimary }]}
|
|
30
|
-
>
|
|
31
|
-
{loadingText}
|
|
32
|
-
</AtomicText>
|
|
33
|
-
)}
|
|
34
|
-
</View>
|
|
35
|
-
</View>
|
|
36
|
-
</Modal>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
PurchaseLoadingOverlay.displayName = "PurchaseLoadingOverlay";
|
|
42
|
-
|
|
43
|
-
const styles = StyleSheet.create({
|
|
44
|
-
container: {
|
|
45
|
-
flex: 1,
|
|
46
|
-
justifyContent: "center",
|
|
47
|
-
alignItems: "center",
|
|
48
|
-
},
|
|
49
|
-
content: {
|
|
50
|
-
paddingHorizontal: 32,
|
|
51
|
-
paddingVertical: 24,
|
|
52
|
-
borderRadius: 16,
|
|
53
|
-
alignItems: "center",
|
|
54
|
-
minWidth: 200,
|
|
55
|
-
},
|
|
56
|
-
text: {
|
|
57
|
-
marginTop: 16,
|
|
58
|
-
textAlign: "center",
|
|
59
|
-
},
|
|
60
|
-
});
|