@umituz/react-native-subscription 2.37.39 → 2.37.41
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/credits/application/CreditLimitCalculator.ts +1 -9
- package/src/domains/credits/application/DeductCreditsCommand.ts +16 -7
- package/src/domains/credits/application/RefundCreditsCommand.ts +1 -5
- package/src/domains/credits/application/credit-strategies/TrialCreditStrategy.ts +1 -5
- package/src/domains/credits/application/creditDocumentHelpers.ts +2 -9
- package/src/domains/credits/core/Credits.ts +0 -23
- package/src/domains/credits/core/CreditsMapper.ts +0 -6
- package/src/domains/credits/core/UserCreditsDocument.ts +0 -12
- package/src/domains/credits/infrastructure/CreditsRepositoryManager.ts +0 -21
- package/src/domains/credits/infrastructure/operations/CreditsWriter.ts +2 -1
- package/src/domains/credits/presentation/deduct-credit/useDeductCredit.ts +2 -2
- package/src/domains/credits/presentation/useCredits.ts +10 -9
- package/src/domains/paywall/components/PaywallContainer.types.ts +0 -28
- package/src/domains/paywall/components/PaywallModal.styles.ts +0 -4
- package/src/domains/paywall/entities/types.ts +0 -5
- package/src/domains/paywall/hooks/usePaywallActions.ts +1 -15
- package/src/domains/revenuecat/core/errors/RevenueCatError.ts +0 -6
- package/src/domains/revenuecat/core/errors/RevenueCatErrorHandler.ts +0 -24
- package/src/domains/revenuecat/core/errors/RevenueCatErrorMessages.ts +0 -18
- package/src/domains/revenuecat/core/errors/index.ts +0 -4
- package/src/domains/revenuecat/core/types/RevenueCatConfig.ts +3 -7
- package/src/domains/revenuecat/core/types/RevenueCatData.ts +4 -9
- package/src/domains/revenuecat/core/types/RevenueCatTypes.ts +5 -65
- package/src/domains/revenuecat/core/types/index.ts +0 -4
- package/src/domains/revenuecat/infrastructure/services/UserSwitchMutex.ts +1 -24
- package/src/domains/subscription/application/SubscriptionAuthListener.ts +5 -21
- package/src/domains/subscription/application/SubscriptionInitializerTypes.ts +1 -5
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +6 -2
- package/src/domains/subscription/application/SubscriptionSyncService.ts +11 -2
- package/src/domains/subscription/application/SubscriptionSyncUtils.ts +1 -1
- package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +15 -2
- package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +9 -2
- package/src/domains/subscription/constants/thresholds.ts +0 -9
- package/src/domains/subscription/core/SubscriptionConstants.ts +0 -4
- package/src/domains/subscription/core/SubscriptionStatus.ts +11 -21
- package/src/domains/subscription/core/SubscriptionStatusHandlers.ts +4 -7
- package/src/domains/subscription/infrastructure/handlers/PurchaseStatusResolver.ts +1 -1
- package/src/domains/subscription/infrastructure/hooks/subscriptionQueryKeys.ts +0 -13
- package/src/domains/subscription/infrastructure/hooks/usePurchasePackage.ts +0 -18
- package/src/domains/subscription/infrastructure/hooks/useRestorePurchase.ts +3 -17
- package/src/domains/subscription/infrastructure/hooks/useRevenueCatTrialEligibility.ts +0 -17
- package/src/domains/subscription/infrastructure/hooks/useSubscriptionPackages.ts +0 -19
- package/src/domains/subscription/infrastructure/hooks/useSubscriptionQueries.ts +0 -6
- package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +0 -17
- package/src/domains/subscription/infrastructure/state/initializationState.ts +0 -25
- package/src/domains/subscription/infrastructure/utils/InitializationCache.ts +0 -21
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +2 -7
- package/src/domains/subscription/infrastructure/utils/authPurchaseState.ts +19 -6
- package/src/domains/subscription/infrastructure/utils/renewal/PackageTierComparator.ts +1 -0
- package/src/domains/subscription/infrastructure/utils/trialEligibilityUtils.ts +0 -18
- package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.styles.ts +0 -5
- package/src/domains/subscription/presentation/components/details/PremiumDetailsCardTypes.ts +0 -5
- package/src/domains/subscription/presentation/components/feedback/paywallFeedbackStyles.ts +0 -5
- package/src/domains/subscription/presentation/stores/index.ts +0 -4
- package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +0 -13
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +35 -21
- package/src/domains/subscription/presentation/usePaywallVisibility.ts +0 -9
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +8 -11
- package/src/domains/subscription/utils/authGuards.ts +3 -0
- package/src/domains/trial/application/TrialService.ts +0 -9
- package/src/domains/trial/core/TrialTypes.ts +0 -8
- package/src/domains/wallet/domain/mappers/TransactionMapper.ts +0 -5
- package/src/domains/wallet/domain/types/transaction.types.ts +0 -7
- package/src/domains/wallet/index.ts +0 -7
- package/src/domains/wallet/infrastructure/config/walletConfig.ts +0 -11
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +6 -3
- package/src/domains/wallet/presentation/hooks/useWallet.ts +0 -7
- package/src/domains/wallet/utils/transactionIconMap.ts +0 -10
- package/src/global.d.ts +0 -6
- package/src/index.ts +1 -4
- package/src/init/createSubscriptionInitModule.ts +12 -2
- package/src/init/index.ts +1 -5
- package/src/presentation/hooks/feedback/useFeedbackSubmit.ts +0 -11
- package/src/shared/application/FeedbackService.ts +3 -21
- package/src/shared/application/ports/ISubscriptionRepository.ts +0 -4
- package/src/shared/infrastructure/SubscriptionEventBus.ts +0 -13
- package/src/shared/infrastructure/firestore/collectionUtils.ts +1 -17
- package/src/shared/infrastructure/firestore/index.ts +0 -4
- package/src/shared/infrastructure/firestore/resultUtils.ts +0 -12
- package/src/shared/infrastructure/react-query/hooks/usePreviousUserCleanup.ts +0 -17
- package/src/shared/infrastructure/react-query/queryConfig.ts +0 -15
- package/src/shared/utils/BaseError.ts +0 -5
- package/src/shared/utils/Result.ts +0 -20
- package/src/shared/utils/dateConverter.ts +6 -46
- package/src/utils/appUtils.ts +0 -16
- package/src/utils/creditMapper.ts +0 -7
- package/src/utils/dateUtils.compare.ts +0 -24
- package/src/utils/dateUtils.core.ts +0 -39
- package/src/utils/dateUtils.format.ts +0 -41
- package/src/utils/dateUtils.math.ts +0 -41
- package/src/utils/dateUtils.ts +0 -5
- package/src/utils/packagePeriodUtils.ts +0 -20
- package/src/utils/packageTypeDetector.ts +1 -21
- package/src/utils/premiumStatusUtils.ts +1 -14
- package/src/utils/priceUtils.ts +0 -35
- package/src/utils/tierUtils.ts +1 -8
- package/src/utils/types.ts +1 -25
- package/src/utils/validation.ts +1 -7
- package/src/domains/README.md +0 -52
- package/src/domains/config/domain/README.md +0 -37
- package/src/domains/config/domain/entities/README.md +0 -41
- package/src/domains/paywall/README.md +0 -101
- package/src/domains/paywall/entities/README.md +0 -40
- package/src/domains/paywall/hooks/README.md +0 -41
- package/src/domains/subscription/application/syncConstants.ts +0 -1
- package/src/domains/subscription/infrastructure/README.md +0 -41
- package/src/domains/subscription/infrastructure/config/README.md +0 -49
- package/src/domains/subscription/infrastructure/handlers/README.md +0 -41
- package/src/domains/subscription/infrastructure/hooks/README.md +0 -50
- package/src/domains/subscription/infrastructure/managers/README.md +0 -41
- package/src/domains/subscription/infrastructure/services/README.md +0 -42
- package/src/domains/subscription/infrastructure/utils/README.md +0 -41
- package/src/domains/subscription/presentation/components/README.md +0 -155
- package/src/domains/subscription/presentation/components/details/CreditRow.md +0 -92
- package/src/domains/subscription/presentation/components/details/DetailRow.md +0 -91
- package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.md +0 -93
- package/src/domains/subscription/presentation/components/details/PremiumStatusBadge.md +0 -91
- package/src/domains/subscription/presentation/components/details/README.md +0 -99
- package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackModal.md +0 -90
- package/src/domains/subscription/presentation/components/feedback/README.md +0 -99
- package/src/domains/subscription/presentation/components/paywall/PaywallModal.md +0 -94
- package/src/domains/subscription/presentation/components/paywall/README.md +0 -54
- package/src/domains/subscription/presentation/components/sections/README.md +0 -99
- package/src/domains/subscription/presentation/components/sections/SubscriptionSection.md +0 -94
- package/src/domains/subscription/presentation/utils/README.md +0 -31
- package/src/domains/wallet/README.md +0 -51
- package/src/domains/wallet/domain/README.md +0 -41
- package/src/domains/wallet/infrastructure/README.md +0 -41
- package/src/domains/wallet/presentation/components/README.md +0 -41
- package/src/domains/wallet/presentation/hooks/README.md +0 -41
- package/src/shared/application/ports/README.md +0 -48
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription Query Keys
|
|
3
|
-
* TanStack Query keys and constants for subscription state
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/** Query cache time constants */
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Query keys for TanStack Query
|
|
11
|
-
*/
|
|
12
1
|
export const SUBSCRIPTION_QUERY_KEYS = {
|
|
13
2
|
packages: ["subscription", "packages"] as const,
|
|
14
3
|
initialized: (userId: string) =>
|
|
15
4
|
["subscription", "initialized", userId] as const,
|
|
16
5
|
customerInfo: ["subscription", "customerInfo"] as const,
|
|
17
6
|
} as const;
|
|
18
|
-
|
|
19
|
-
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purchase Package Hook
|
|
3
|
-
* TanStack mutation for purchasing subscription packages
|
|
4
|
-
* Credits are initialized by CustomerInfoListener (not here to avoid duplicates)
|
|
5
|
-
* Auth info automatically read from @umituz/react-native-auth
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { useMutation, useQueryClient } from "@umituz/react-native-design-system";
|
|
9
2
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
10
3
|
import { useAlert } from "@umituz/react-native-design-system";
|
|
@@ -19,17 +12,11 @@ import { subscriptionStatusQueryKeys } from "../../presentation/useSubscriptionS
|
|
|
19
12
|
import { creditsQueryKeys } from "../../../credits/presentation/creditsQueryKeys";
|
|
20
13
|
import { getErrorMessage } from "../../../revenuecat/core/errors";
|
|
21
14
|
|
|
22
|
-
/** Purchase mutation result - simplified for presentation layer */
|
|
23
15
|
interface PurchaseMutationResult {
|
|
24
16
|
success: boolean;
|
|
25
17
|
productId: string;
|
|
26
18
|
}
|
|
27
19
|
|
|
28
|
-
/**
|
|
29
|
-
* Purchase a subscription package
|
|
30
|
-
* Credits are initialized by CustomerInfoListener when entitlement becomes active
|
|
31
|
-
* Auth info automatically read from auth store
|
|
32
|
-
*/
|
|
33
20
|
export const usePurchasePackage = () => {
|
|
34
21
|
const userId = useAuthStore(selectUserId);
|
|
35
22
|
const isAnonymous = useAuthStore(selectIsAnonymous);
|
|
@@ -51,13 +38,11 @@ export const usePurchasePackage = () => {
|
|
|
51
38
|
console.log(`[Purchase] Initializing and purchasing. User: ${userId}`);
|
|
52
39
|
}
|
|
53
40
|
|
|
54
|
-
await SubscriptionManager.initialize(userId);
|
|
55
41
|
const success = await SubscriptionManager.purchasePackage(pkg, userId);
|
|
56
42
|
|
|
57
43
|
return { success, productId };
|
|
58
44
|
},
|
|
59
45
|
onSuccess: (result) => {
|
|
60
|
-
|
|
61
46
|
if (result.success) {
|
|
62
47
|
showSuccess("Purchase Successful", "Your subscription is now active!");
|
|
63
48
|
queryClient.invalidateQueries({
|
|
@@ -76,11 +61,8 @@ export const usePurchasePackage = () => {
|
|
|
76
61
|
}
|
|
77
62
|
},
|
|
78
63
|
onError: (error) => {
|
|
79
|
-
|
|
80
|
-
// Use map-based lookup - O(1) complexity
|
|
81
64
|
const errorInfo = getErrorMessage(error);
|
|
82
65
|
|
|
83
|
-
// Don't show alert for user cancellation
|
|
84
66
|
if (!errorInfo.shouldShowAlert) {
|
|
85
67
|
return;
|
|
86
68
|
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Restore Purchase Hook
|
|
3
|
-
* TanStack mutation for restoring previous purchases
|
|
4
|
-
* Credits are initialized by CustomerInfoListener (not here to avoid duplicates)
|
|
5
|
-
* Auth info automatically read from @umituz/react-native-auth
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { useMutation, useQueryClient } from "@umituz/react-native-design-system";
|
|
9
2
|
import { useAlert } from "@umituz/react-native-design-system";
|
|
10
3
|
import {
|
|
11
4
|
useAuthStore,
|
|
12
5
|
selectUserId,
|
|
6
|
+
selectIsAnonymous,
|
|
13
7
|
} from "@umituz/react-native-auth";
|
|
14
8
|
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
15
9
|
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
@@ -22,29 +16,23 @@ interface RestoreResult {
|
|
|
22
16
|
productId: string | null;
|
|
23
17
|
}
|
|
24
18
|
|
|
25
|
-
/**
|
|
26
|
-
* Restore previous purchases
|
|
27
|
-
* Credits are initialized by CustomerInfoListener when entitlement becomes active
|
|
28
|
-
* Auth info automatically read from auth store
|
|
29
|
-
*/
|
|
30
19
|
export const useRestorePurchase = () => {
|
|
31
20
|
const userId = useAuthStore(selectUserId);
|
|
21
|
+
const isAnonymous = useAuthStore(selectIsAnonymous);
|
|
32
22
|
const queryClient = useQueryClient();
|
|
33
23
|
const { showSuccess, showInfo, showError } = useAlert();
|
|
34
24
|
|
|
35
25
|
return useMutation({
|
|
36
26
|
mutationFn: async (): Promise<RestoreResult> => {
|
|
37
|
-
if (!userId) {
|
|
27
|
+
if (!userId || isAnonymous) {
|
|
38
28
|
throw new Error("User not authenticated");
|
|
39
29
|
}
|
|
40
30
|
|
|
41
|
-
await SubscriptionManager.initialize(userId);
|
|
42
31
|
const result = await SubscriptionManager.restore(userId);
|
|
43
32
|
return result;
|
|
44
33
|
},
|
|
45
34
|
onSuccess: (result) => {
|
|
46
35
|
if (result.success) {
|
|
47
|
-
// Invalidate queries to refresh data
|
|
48
36
|
queryClient.invalidateQueries({
|
|
49
37
|
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
50
38
|
});
|
|
@@ -57,7 +45,6 @@ export const useRestorePurchase = () => {
|
|
|
57
45
|
});
|
|
58
46
|
}
|
|
59
47
|
|
|
60
|
-
// Show user feedback
|
|
61
48
|
if (result.productId) {
|
|
62
49
|
showSuccess("Restore Successful", "Your subscription has been restored!");
|
|
63
50
|
} else {
|
|
@@ -66,7 +53,6 @@ export const useRestorePurchase = () => {
|
|
|
66
53
|
}
|
|
67
54
|
},
|
|
68
55
|
onError: (error) => {
|
|
69
|
-
// Use map-based lookup - O(1) complexity
|
|
70
56
|
const errorInfo = getErrorMessage(error);
|
|
71
57
|
showError(errorInfo.title, errorInfo.message);
|
|
72
58
|
},
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useRevenueCatTrialEligibility Hook
|
|
3
|
-
* Checks if user is eligible for introductory offers via RevenueCat
|
|
4
|
-
* Uses Apple's native mechanism for trial eligibility
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
8
2
|
import { getRevenueCatService } from "../services/RevenueCatService";
|
|
9
3
|
import {
|
|
@@ -14,24 +8,14 @@ import {
|
|
|
14
8
|
type TrialEligibilityMap,
|
|
15
9
|
} from "../utils/trialEligibilityUtils";
|
|
16
10
|
|
|
17
|
-
|
|
18
11
|
interface UseRevenueCatTrialEligibilityResult {
|
|
19
|
-
/** Map of product IDs to their trial eligibility */
|
|
20
12
|
eligibilityMap: TrialEligibilityMap;
|
|
21
|
-
/** Whether eligibility check is in progress */
|
|
22
13
|
isLoading: boolean;
|
|
23
|
-
/** Whether any product has an eligible trial */
|
|
24
14
|
hasEligibleTrial: boolean;
|
|
25
|
-
/** Check eligibility for specific product IDs */
|
|
26
15
|
checkEligibility: (productIds: string[]) => Promise<void>;
|
|
27
|
-
/** Get eligibility for a specific product */
|
|
28
16
|
getProductEligibility: (productId: string) => ProductTrialEligibility | null;
|
|
29
17
|
}
|
|
30
18
|
|
|
31
|
-
/**
|
|
32
|
-
* Hook to check trial eligibility via RevenueCat
|
|
33
|
-
* Uses Apple's introductory offer eligibility system
|
|
34
|
-
*/
|
|
35
19
|
export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityResult {
|
|
36
20
|
const [eligibilityMap, setEligibilityMap] = useState<TrialEligibilityMap>({});
|
|
37
21
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -95,4 +79,3 @@ export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityRe
|
|
|
95
79
|
getProductEligibility,
|
|
96
80
|
};
|
|
97
81
|
}
|
|
98
|
-
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription Packages Hook
|
|
3
|
-
* TanStack query for fetching available packages (offerings)
|
|
4
|
-
* Auth info automatically read from @umituz/react-native-auth
|
|
5
|
-
*
|
|
6
|
-
* IMPORTANT: Packages (offerings) are NOT user-specific - they're the same
|
|
7
|
-
* for all users. We only need RevenueCat to be initialized, not necessarily
|
|
8
|
-
* for a specific user. User-specific checks belong in useSubscriptionStatus.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
1
|
import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
|
|
12
2
|
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
13
3
|
import {
|
|
@@ -20,27 +10,18 @@ import {
|
|
|
20
10
|
SUBSCRIPTION_QUERY_KEYS,
|
|
21
11
|
} from "./subscriptionQueryKeys";
|
|
22
12
|
|
|
23
|
-
/**
|
|
24
|
-
* Fetch available subscription packages
|
|
25
|
-
* Works for both authenticated and anonymous users
|
|
26
|
-
* Auth info automatically read from auth store
|
|
27
|
-
*/
|
|
28
13
|
export const useSubscriptionPackages = () => {
|
|
29
14
|
const userId = useAuthStore(selectUserId);
|
|
30
15
|
const isConfigured = SubscriptionManager.isConfigured();
|
|
31
16
|
const queryClient = useQueryClient();
|
|
32
17
|
const prevUserIdRef = useRef(userId);
|
|
33
18
|
|
|
34
|
-
// Reactive initialization state - triggers re-render when BackgroundInitializer completes
|
|
35
19
|
const initState = useSyncExternalStore(
|
|
36
20
|
initializationState.subscribe,
|
|
37
21
|
initializationState.getSnapshot,
|
|
38
22
|
initializationState.getSnapshot,
|
|
39
23
|
);
|
|
40
24
|
|
|
41
|
-
// Packages (offerings) are NOT user-specific - same for all users.
|
|
42
|
-
// We only need RevenueCat to be initialized at all.
|
|
43
|
-
// Use reactive state OR direct manager check for backwards compatibility.
|
|
44
25
|
const isInitialized = initState.initialized || SubscriptionManager.isInitialized();
|
|
45
26
|
|
|
46
27
|
const query = useQuery({
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription TanStack Query Hooks
|
|
3
|
-
* Server state management for RevenueCat subscriptions
|
|
4
|
-
* Generic hooks for 100+ apps
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
export { useSubscriptionPackages } from "./useSubscriptionPackages";
|
|
8
2
|
export { usePurchasePackage } from "./usePurchasePackage";
|
|
9
3
|
export { useRestorePurchase } from "./useRestorePurchase";
|
|
@@ -1,25 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription Manager Utilities
|
|
3
|
-
* Validation and helper functions for SubscriptionManager
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import type { SubscriptionManagerConfig } from "./SubscriptionManager.types";
|
|
7
2
|
|
|
8
3
|
import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
|
|
9
4
|
import { SubscriptionInternalState } from "./SubscriptionInternalState";
|
|
10
5
|
|
|
11
|
-
/**
|
|
12
|
-
* Validate that manager is configured
|
|
13
|
-
*/
|
|
14
6
|
export function ensureConfigured(config: SubscriptionManagerConfig | null): void {
|
|
15
7
|
if (!config) {
|
|
16
8
|
throw new Error("SubscriptionManager not configured");
|
|
17
9
|
}
|
|
18
10
|
}
|
|
19
11
|
|
|
20
|
-
/**
|
|
21
|
-
* Get current user ID or throw
|
|
22
|
-
*/
|
|
23
12
|
export function getCurrentUserIdOrThrow(state: SubscriptionInternalState): string {
|
|
24
13
|
const userId = state.initCache.getCurrentUserId();
|
|
25
14
|
if (userId === null || userId === undefined) {
|
|
@@ -28,9 +17,6 @@ export function getCurrentUserIdOrThrow(state: SubscriptionInternalState): strin
|
|
|
28
17
|
return userId;
|
|
29
18
|
}
|
|
30
19
|
|
|
31
|
-
/**
|
|
32
|
-
* Get service instance or initialize
|
|
33
|
-
*/
|
|
34
20
|
export function getOrCreateService(
|
|
35
21
|
currentInstance: IRevenueCatService | null
|
|
36
22
|
): IRevenueCatService {
|
|
@@ -48,9 +34,6 @@ export function getOrCreateService(
|
|
|
48
34
|
return serviceInstance;
|
|
49
35
|
}
|
|
50
36
|
|
|
51
|
-
/**
|
|
52
|
-
* Validate service is available
|
|
53
|
-
*/
|
|
54
37
|
export function ensureServiceAvailable(service: IRevenueCatService | null): void {
|
|
55
38
|
if (!service) {
|
|
56
39
|
throw new Error("Service instance not available");
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reactive Initialization State
|
|
3
|
-
* Uses useSyncExternalStore pattern to make SubscriptionManager
|
|
4
|
-
* initialization state reactive for React components.
|
|
5
|
-
*
|
|
6
|
-
* Problem: SubscriptionManager.isInitializedForUser() is a plain method call.
|
|
7
|
-
* When BackgroundInitializer completes (500ms+ after auth), React components
|
|
8
|
-
* don't re-render because there's no reactive state change.
|
|
9
|
-
*
|
|
10
|
-
* Solution: This module provides a subscribe/getSnapshot interface that
|
|
11
|
-
* React's useSyncExternalStore can use to trigger re-renders when
|
|
12
|
-
* initialization completes.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
1
|
type Listener = () => void;
|
|
16
2
|
|
|
17
3
|
interface InitState {
|
|
@@ -34,27 +20,16 @@ export const initializationState = {
|
|
|
34
20
|
|
|
35
21
|
getSnapshot: (): InitState => state,
|
|
36
22
|
|
|
37
|
-
/**
|
|
38
|
-
* Called by SubscriptionManager after successful initialization.
|
|
39
|
-
* Triggers re-render in all subscribed React components.
|
|
40
|
-
*/
|
|
41
23
|
markInitialized: (userId: string | null): void => {
|
|
42
24
|
state = { initialized: true, userId };
|
|
43
25
|
notifyListeners();
|
|
44
26
|
},
|
|
45
27
|
|
|
46
|
-
/**
|
|
47
|
-
* Called when initialization starts for a new user (e.g., user switch).
|
|
48
|
-
* Resets the state so queries know they need to wait.
|
|
49
|
-
*/
|
|
50
28
|
markPending: (): void => {
|
|
51
29
|
state = { initialized: false, userId: null };
|
|
52
30
|
notifyListeners();
|
|
53
31
|
},
|
|
54
32
|
|
|
55
|
-
/**
|
|
56
|
-
* Check if initialized for a specific user.
|
|
57
|
-
*/
|
|
58
33
|
isInitializedForUser: (userId: string | null): boolean => {
|
|
59
34
|
return state.initialized && state.userId === userId;
|
|
60
35
|
},
|
|
@@ -1,31 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Initialization Cache
|
|
3
|
-
* Manages promise caching and user state for initialization
|
|
4
|
-
* Thread-safe: Uses atomic promise-based locking pattern
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
export class InitializationCache {
|
|
8
2
|
private initPromise: Promise<boolean> | null = null;
|
|
9
3
|
private currentUserId: string | null = null;
|
|
10
|
-
// Track which userId the promise is for
|
|
11
4
|
private promiseUserId: string | null = null;
|
|
12
|
-
// Track promise completion state
|
|
13
5
|
private promiseCompleted = true;
|
|
14
|
-
// Pending initialization queue
|
|
15
6
|
private pendingQueue: Map<string, Promise<boolean>> = new Map();
|
|
16
7
|
|
|
17
|
-
/**
|
|
18
|
-
* Atomically check if reinitialization is needed AND reserve the slot
|
|
19
|
-
* Returns: { shouldInit: boolean, existingPromise: Promise | null }
|
|
20
|
-
*/
|
|
21
8
|
tryAcquireInitialization(userId: string): { shouldInit: boolean; existingPromise: Promise<boolean> | null } {
|
|
22
|
-
// Check if there's already a pending promise for this user in the queue
|
|
23
9
|
const queuedPromise = this.pendingQueue.get(userId);
|
|
24
10
|
if (queuedPromise) {
|
|
25
11
|
return { shouldInit: false, existingPromise: queuedPromise };
|
|
26
12
|
}
|
|
27
13
|
|
|
28
|
-
// If already initialized for this user and promise completed successfully
|
|
29
14
|
if (
|
|
30
15
|
this.initPromise &&
|
|
31
16
|
this.currentUserId === userId &&
|
|
@@ -35,7 +20,6 @@ export class InitializationCache {
|
|
|
35
20
|
return { shouldInit: false, existingPromise: this.initPromise };
|
|
36
21
|
}
|
|
37
22
|
|
|
38
|
-
// Different user or not initialized - need to initialize
|
|
39
23
|
return { shouldInit: true, existingPromise: null };
|
|
40
24
|
}
|
|
41
25
|
|
|
@@ -45,9 +29,6 @@ export class InitializationCache {
|
|
|
45
29
|
|
|
46
30
|
const targetUserId = userId;
|
|
47
31
|
|
|
48
|
-
// Build the handled chain that ALWAYS resolves (never rejects).
|
|
49
|
-
// This is critical: pendingQueue must store a non-rejectable promise so that
|
|
50
|
-
// callers who receive it via tryAcquireInitialization never get an unhandled rejection.
|
|
51
32
|
const chain: Promise<boolean> = promise
|
|
52
33
|
.then((result) => {
|
|
53
34
|
if (result && this.promiseUserId === targetUserId) {
|
|
@@ -67,11 +48,9 @@ export class InitializationCache {
|
|
|
67
48
|
return false as boolean;
|
|
68
49
|
})
|
|
69
50
|
.finally(() => {
|
|
70
|
-
// Remove from queue when complete
|
|
71
51
|
this.pendingQueue.delete(targetUserId);
|
|
72
52
|
});
|
|
73
53
|
|
|
74
|
-
// Store the chain (not the original promise) so callers never receive a rejection
|
|
75
54
|
this.initPromise = chain;
|
|
76
55
|
this.pendingQueue.set(userId, chain);
|
|
77
56
|
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Premium Status Syncer
|
|
3
|
-
* Syncs premium status to database via callbacks
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import type { CustomerInfo } from "react-native-purchases";
|
|
7
2
|
import type { RevenueCatConfig, PackageType } from "../../../revenuecat/core/types";
|
|
8
3
|
import type { PurchaseSource } from "../../../subscription/core/SubscriptionConstants";
|
|
@@ -85,7 +80,7 @@ export async function notifyRestoreCompleted(
|
|
|
85
80
|
|
|
86
81
|
try {
|
|
87
82
|
await config.onRestoreCompleted(userId, isPremium, customerInfo);
|
|
88
|
-
} catch (
|
|
89
|
-
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('[PremiumStatusSyncer] Restore callback failed:', error instanceof Error ? error.message : String(error));
|
|
90
85
|
}
|
|
91
86
|
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth Purchase State Manager
|
|
3
|
-
* Manages global state for auth-aware purchase operations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
2
|
import type { PurchaseSource } from "../../core/SubscriptionConstants";
|
|
8
3
|
|
|
@@ -15,9 +10,12 @@ interface SavedPurchaseState {
|
|
|
15
10
|
pkg: PurchasesPackage;
|
|
16
11
|
source: PurchaseSource;
|
|
17
12
|
timestamp: number;
|
|
13
|
+
sessionId: string;
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
const SAVED_PURCHASE_EXPIRY_MS =
|
|
16
|
+
const SAVED_PURCHASE_EXPIRY_MS = 2 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
let currentSessionId = "";
|
|
21
19
|
|
|
22
20
|
class AuthPurchaseStateManager {
|
|
23
21
|
private authProvider: PurchaseAuthProvider | null = null;
|
|
@@ -31,11 +29,16 @@ class AuthPurchaseStateManager {
|
|
|
31
29
|
return this.authProvider;
|
|
32
30
|
}
|
|
33
31
|
|
|
32
|
+
setSessionId(sessionId: string): void {
|
|
33
|
+
currentSessionId = sessionId;
|
|
34
|
+
}
|
|
35
|
+
|
|
34
36
|
savePurchase(pkg: PurchasesPackage, source: PurchaseSource): void {
|
|
35
37
|
this.savedPurchaseState = {
|
|
36
38
|
pkg,
|
|
37
39
|
source,
|
|
38
40
|
timestamp: Date.now(),
|
|
41
|
+
sessionId: currentSessionId,
|
|
39
42
|
};
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -44,6 +47,11 @@ class AuthPurchaseStateManager {
|
|
|
44
47
|
return null;
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
if (this.savedPurchaseState.sessionId !== currentSessionId) {
|
|
51
|
+
this.savedPurchaseState = null;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
const isExpired = Date.now() - this.savedPurchaseState.timestamp > SAVED_PURCHASE_EXPIRY_MS;
|
|
48
56
|
if (isExpired) {
|
|
49
57
|
this.savedPurchaseState = null;
|
|
@@ -60,9 +68,14 @@ class AuthPurchaseStateManager {
|
|
|
60
68
|
this.savedPurchaseState = null;
|
|
61
69
|
}
|
|
62
70
|
|
|
71
|
+
onUserChanged(): void {
|
|
72
|
+
this.savedPurchaseState = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
cleanup(): void {
|
|
64
76
|
this.authProvider = null;
|
|
65
77
|
this.savedPurchaseState = null;
|
|
78
|
+
currentSessionId = "";
|
|
66
79
|
}
|
|
67
80
|
}
|
|
68
81
|
|
|
@@ -1,29 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Trial Eligibility Utilities
|
|
3
|
-
* Business logic for checking trial eligibility
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import Purchases, {
|
|
7
2
|
type IntroEligibility,
|
|
8
3
|
INTRO_ELIGIBILITY_STATUS,
|
|
9
4
|
} from "react-native-purchases";
|
|
10
5
|
|
|
11
|
-
/** Trial eligibility info for a single product */
|
|
12
6
|
export interface ProductTrialEligibility {
|
|
13
7
|
productId: string;
|
|
14
8
|
eligible: boolean;
|
|
15
9
|
trialDurationDays?: number;
|
|
16
10
|
}
|
|
17
11
|
|
|
18
|
-
/** Map of product ID to eligibility */
|
|
19
12
|
export type TrialEligibilityMap = Record<string, ProductTrialEligibility>;
|
|
20
13
|
|
|
21
|
-
/** Default trial duration in days */
|
|
22
14
|
const DEFAULT_TRIAL_DURATION_DAYS = 7;
|
|
23
15
|
|
|
24
|
-
/**
|
|
25
|
-
* Check trial eligibility for product IDs
|
|
26
|
-
*/
|
|
27
16
|
export async function checkTrialEligibility(
|
|
28
17
|
productIds: string[]
|
|
29
18
|
): Promise<TrialEligibilityMap> {
|
|
@@ -47,10 +36,6 @@ export async function checkTrialEligibility(
|
|
|
47
36
|
return result;
|
|
48
37
|
}
|
|
49
38
|
|
|
50
|
-
/**
|
|
51
|
-
* Create fallback eligibility map (all eligible)
|
|
52
|
-
* Used when eligibility check fails
|
|
53
|
-
*/
|
|
54
39
|
export function createFallbackEligibilityMap(
|
|
55
40
|
productIds: string[]
|
|
56
41
|
): TrialEligibilityMap {
|
|
@@ -67,9 +52,6 @@ export function createFallbackEligibilityMap(
|
|
|
67
52
|
return result;
|
|
68
53
|
}
|
|
69
54
|
|
|
70
|
-
/**
|
|
71
|
-
* Check if any product has eligible trial
|
|
72
|
-
*/
|
|
73
55
|
export function hasAnyEligibleTrial(
|
|
74
56
|
eligibilityMap: TrialEligibilityMap
|
|
75
57
|
): boolean {
|
|
@@ -1,25 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purchase Loading Store
|
|
3
|
-
* Global state for tracking purchase loading across the app
|
|
4
|
-
* Supports concurrent purchases via Map-based tracking
|
|
5
|
-
* Used by both PaywallModal and useSavedPurchaseAutoExecution
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { create } from "zustand";
|
|
9
2
|
|
|
10
3
|
interface PurchaseLoadingState {
|
|
11
|
-
/** Map of product IDs to purchase sources (supports concurrent purchases) */
|
|
12
4
|
activePurchases: Map<string, "manual" | "auto-execution">;
|
|
13
5
|
}
|
|
14
6
|
|
|
15
7
|
interface PurchaseLoadingActions {
|
|
16
|
-
/** Start purchase loading state for a product */
|
|
17
8
|
startPurchase: (productId: string, source: "manual" | "auto-execution") => void;
|
|
18
|
-
/** End purchase loading state for a product */
|
|
19
9
|
endPurchase: (productId: string) => void;
|
|
20
|
-
/** Check if any purchase is in progress, or if a specific product is being purchased */
|
|
21
10
|
isPurchasing: (productId?: string) => boolean;
|
|
22
|
-
/** Reset all state */
|
|
23
11
|
reset: () => void;
|
|
24
12
|
}
|
|
25
13
|
|
|
@@ -59,5 +47,4 @@ export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set, get) =
|
|
|
59
47
|
},
|
|
60
48
|
}));
|
|
61
49
|
|
|
62
|
-
// Selectors for optimized re-renders
|
|
63
50
|
export const selectIsPurchasing = (state: PurchaseLoadingStore) => state.activePurchases.size > 0;
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Auth-Aware Purchase Hook
|
|
3
|
-
* Handles purchase flow with authentication requirement
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useCallback } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
7
2
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
8
3
|
import { usePremium } from "./usePremium";
|
|
9
4
|
import type { PurchaseSource } from "../core/SubscriptionConstants";
|
|
@@ -35,10 +30,43 @@ export const useAuthAwarePurchase = (
|
|
|
35
30
|
params?: UseAuthAwarePurchaseParams
|
|
36
31
|
): UseAuthAwarePurchaseResult => {
|
|
37
32
|
const { purchasePackage, restorePurchase } = usePremium();
|
|
33
|
+
const isExecutingSavedRef = useRef(false);
|
|
34
|
+
|
|
35
|
+
const executeSavedPurchase = useCallback(async (): Promise<boolean> => {
|
|
36
|
+
const saved = authPurchaseStateManager.getSavedPurchase();
|
|
37
|
+
if (!saved) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const result = await purchasePackage(saved.pkg);
|
|
43
|
+
if (result) {
|
|
44
|
+
authPurchaseStateManager.clearSavedPurchase();
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
} catch {
|
|
48
|
+
authPurchaseStateManager.clearSavedPurchase();
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}, [purchasePackage]);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const authProvider = authPurchaseStateManager.getProvider();
|
|
55
|
+
if (!authProvider) return;
|
|
56
|
+
|
|
57
|
+
const isAuth = authProvider.isAuthenticated();
|
|
58
|
+
const hasSavedPurchase = !!authPurchaseStateManager.getSavedPurchase();
|
|
59
|
+
|
|
60
|
+
if (isAuth && hasSavedPurchase && !isExecutingSavedRef.current) {
|
|
61
|
+
isExecutingSavedRef.current = true;
|
|
62
|
+
executeSavedPurchase().finally(() => {
|
|
63
|
+
isExecutingSavedRef.current = false;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}, [executeSavedPurchase]);
|
|
38
67
|
|
|
39
68
|
const handlePurchase = useCallback(
|
|
40
69
|
async (pkg: PurchasesPackage, source?: PurchaseSource): Promise<boolean> => {
|
|
41
|
-
|
|
42
70
|
const authProvider = authPurchaseStateManager.getProvider();
|
|
43
71
|
|
|
44
72
|
if (!authProvider) {
|
|
@@ -61,7 +89,6 @@ export const useAuthAwarePurchase = (
|
|
|
61
89
|
);
|
|
62
90
|
|
|
63
91
|
const handleRestore = useCallback(async (): Promise<boolean> => {
|
|
64
|
-
|
|
65
92
|
const authProvider = authPurchaseStateManager.getProvider();
|
|
66
93
|
|
|
67
94
|
if (!authProvider) {
|
|
@@ -78,19 +105,6 @@ export const useAuthAwarePurchase = (
|
|
|
78
105
|
return result;
|
|
79
106
|
}, [restorePurchase]);
|
|
80
107
|
|
|
81
|
-
const executeSavedPurchase = useCallback(async (): Promise<boolean> => {
|
|
82
|
-
const saved = authPurchaseStateManager.getSavedPurchase();
|
|
83
|
-
if (!saved) {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const result = await purchasePackage(saved.pkg);
|
|
88
|
-
if (result) {
|
|
89
|
-
authPurchaseStateManager.clearSavedPurchase();
|
|
90
|
-
}
|
|
91
|
-
return result;
|
|
92
|
-
}, [purchasePackage]);
|
|
93
|
-
|
|
94
108
|
return {
|
|
95
109
|
handlePurchase,
|
|
96
110
|
handleRestore,
|