@umituz/react-native-subscription 2.27.95 → 2.27.97
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/core/Credits.ts +3 -11
- package/src/domains/credits/infrastructure/CreditsRepository.ts +28 -1
- package/src/domains/paywall/components/PaywallContainer.tsx +17 -1
- package/src/domains/paywall/components/PaywallContainer.types.ts +2 -1
- package/src/domains/paywall/hooks/usePaywallActions.ts +1 -1
- package/src/domains/subscription/application/SubscriptionInitializer.ts +1 -1
- package/src/domains/subscription/application/SubscriptionSyncService.ts +32 -5
- package/src/domains/subscription/application/SubscriptionSyncUtils.ts +1 -1
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +32 -12
- package/src/domains/subscription/infrastructure/hooks/subscriptionQueryKeys.ts +4 -4
- package/src/domains/subscription/infrastructure/hooks/useSubscriptionPackages.ts +1 -7
- package/src/domains/subscription/infrastructure/hooks/useSubscriptionQueries.ts +0 -2
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +5 -4
- package/src/domains/subscription/infrastructure/utils/InitializationCache.ts +20 -7
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +1 -1
- package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.tsx +55 -16
- package/src/domains/subscription/presentation/screens/components/CreditsList.tsx +14 -1
- package/src/domains/subscription/presentation/screens/components/DevTestSection.tsx +10 -2
- package/src/domains/subscription/presentation/screens/components/SubscriptionActions.tsx +6 -1
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.tsx +20 -1
- package/src/domains/subscription/presentation/screens/components/UpgradePrompt.tsx +13 -1
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +1 -1
- package/src/domains/subscription/presentation/useFeatureGate.ts +11 -7
- package/src/domains/subscription/presentation/usePaywallVisibility.ts +1 -1
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +1 -5
- package/src/init/index.ts +0 -3
- package/src/presentation/hooks/index.ts +0 -4
- package/src/shared/infrastructure/SubscriptionEventBus.ts +27 -0
- package/src/types/i18next.d.ts +2 -0
- package/src/utils/packageTypeDetector.ts +0 -4
- package/src/domains/subscription/presentation/types/README.md +0 -22
- package/src/domains/subscription/presentation/types/SubscriptionDetailTypes.ts +0 -153
- package/src/domains/subscription/presentation/types/SubscriptionSettingsTypes.ts +0 -74
- package/src/domains/subscription/presentation/useAuthSubscriptionSync.ts +0 -63
- package/src/domains/subscription/presentation/usePremiumGate.ts +0 -84
- package/src/domains/subscription/presentation/useSavedPurchaseAutoExecution.ts +0 -148
- package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.ts +0 -115
- package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.utils.ts +0 -57
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.27.
|
|
3
|
+
"version": "2.27.97",
|
|
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",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { SubscriptionPackageType } from "../../../utils/packageTypeDetector";
|
|
9
|
+
// Types imported from SubscriptionConstants are used directly in UserCredits interface
|
|
9
10
|
import type {
|
|
10
11
|
SubscriptionStatusType,
|
|
11
12
|
PeriodType,
|
|
@@ -15,15 +16,6 @@ import type {
|
|
|
15
16
|
PurchaseType
|
|
16
17
|
} from "../../subscription/core/SubscriptionConstants";
|
|
17
18
|
|
|
18
|
-
export type {
|
|
19
|
-
SubscriptionStatusType,
|
|
20
|
-
PeriodType,
|
|
21
|
-
PackageType,
|
|
22
|
-
Platform,
|
|
23
|
-
PurchaseSource,
|
|
24
|
-
PurchaseType
|
|
25
|
-
};
|
|
26
|
-
|
|
27
19
|
export type CreditType = "text" | "image";
|
|
28
20
|
|
|
29
21
|
/** Single Source of Truth for user subscription + credits data */
|
|
@@ -67,10 +59,10 @@ export interface CreditAllocation {
|
|
|
67
59
|
credits: number;
|
|
68
60
|
}
|
|
69
61
|
|
|
70
|
-
export type PackageAllocationMap = Record<
|
|
62
|
+
export type PackageAllocationMap = Partial<Record<
|
|
71
63
|
Exclude<SubscriptionPackageType, "unknown">,
|
|
72
64
|
CreditAllocation
|
|
73
|
-
|
|
65
|
+
>>;
|
|
74
66
|
|
|
75
67
|
export interface CreditsConfig {
|
|
76
68
|
collectionName: string;
|
|
@@ -12,6 +12,8 @@ import { CreditsMapper } from "../core/CreditsMapper";
|
|
|
12
12
|
import type { RevenueCatData } from "../../subscription/core/RevenueCatData";
|
|
13
13
|
import { DeductCreditsCommand } from "../application/DeductCreditsCommand";
|
|
14
14
|
import { CreditLimitCalculator } from "../application/CreditLimitCalculator";
|
|
15
|
+
import { PURCHASE_TYPE, type PurchaseType } from "../../subscription/core/SubscriptionConstants";
|
|
16
|
+
import { updateDoc } from "firebase/firestore";
|
|
15
17
|
|
|
16
18
|
export class CreditsRepository extends BaseRepository {
|
|
17
19
|
private deductCommand: DeductCreditsCommand;
|
|
@@ -47,7 +49,8 @@ export class CreditsRepository extends BaseRepository {
|
|
|
47
49
|
purchaseId: string,
|
|
48
50
|
productId: string,
|
|
49
51
|
source: PurchaseSource,
|
|
50
|
-
revenueCatData: RevenueCatData
|
|
52
|
+
revenueCatData: RevenueCatData,
|
|
53
|
+
type: PurchaseType = PURCHASE_TYPE.INITIAL
|
|
51
54
|
): Promise<CreditsResult> {
|
|
52
55
|
const db = getFirestore();
|
|
53
56
|
if (!db) {
|
|
@@ -70,6 +73,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
70
73
|
originalTransactionId: revenueCatData.originalTransactionId,
|
|
71
74
|
isPremium: revenueCatData.isPremium,
|
|
72
75
|
periodType: revenueCatData.periodType,
|
|
76
|
+
type,
|
|
73
77
|
}
|
|
74
78
|
);
|
|
75
79
|
|
|
@@ -86,4 +90,27 @@ export class CreditsRepository extends BaseRepository {
|
|
|
86
90
|
async deductCredit(userId: string, cost: number): Promise<DeductCreditsResult> {
|
|
87
91
|
return this.deductCommand.execute(userId, cost);
|
|
88
92
|
}
|
|
93
|
+
|
|
94
|
+
async hasCredits(userId: string, cost: number): Promise<boolean> {
|
|
95
|
+
const result = await this.getCredits(userId);
|
|
96
|
+
if (!result.success || !result.data) return false;
|
|
97
|
+
return result.data.credits >= cost;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async syncExpiredStatus(userId: string): Promise<void> {
|
|
101
|
+
const db = getFirestore();
|
|
102
|
+
if (!db) throw new Error("Firestore instance is not available");
|
|
103
|
+
|
|
104
|
+
const ref = this.getRef(db, userId);
|
|
105
|
+
await updateDoc(ref, {
|
|
106
|
+
isPremium: false,
|
|
107
|
+
status: "expired",
|
|
108
|
+
willRenew: false,
|
|
109
|
+
expirationDate: new Date().toISOString()
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function createCreditsRepository(config: CreditsConfig): CreditsRepository {
|
|
115
|
+
return new CreditsRepository(config);
|
|
89
116
|
}
|
|
@@ -49,9 +49,25 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
// Check trial eligibility only if trialConfig is enabled
|
|
52
|
+
// Use ref to track if we've already checked for these packages to avoid redundant calls
|
|
53
|
+
const checkedPackagesRef = React.useRef<string[]>([]);
|
|
54
|
+
|
|
52
55
|
useEffect(() => {
|
|
53
56
|
if (!trialConfig?.enabled) return;
|
|
54
57
|
if (packages.length === 0) return;
|
|
58
|
+
if (isLoading) return; // Wait for packages to fully load
|
|
59
|
+
|
|
60
|
+
// Get current package identifiers
|
|
61
|
+
const currentPackageIds = packages.map((pkg) => pkg.product.identifier);
|
|
62
|
+
const sortedIds = [...currentPackageIds].sort().join(",");
|
|
63
|
+
|
|
64
|
+
// Skip if we've already checked these exact packages
|
|
65
|
+
if (checkedPackagesRef.current.join(",") === sortedIds) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Update ref
|
|
70
|
+
checkedPackagesRef.current = currentPackageIds;
|
|
55
71
|
|
|
56
72
|
// Get all actual product IDs from packages
|
|
57
73
|
const allProductIds = packages.map((pkg) => pkg.product.identifier);
|
|
@@ -72,7 +88,7 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
72
88
|
if (productIdsToCheck.length > 0) {
|
|
73
89
|
checkEligibility(productIdsToCheck);
|
|
74
90
|
}
|
|
75
|
-
}, [packages, checkEligibility, trialConfig?.enabled, trialConfig?.eligibleProductIds]);
|
|
91
|
+
}, [packages, isLoading, checkEligibility, trialConfig?.enabled, trialConfig?.eligibleProductIds]);
|
|
76
92
|
|
|
77
93
|
// Convert eligibility map to format expected by PaywallModal
|
|
78
94
|
// Only process if trial is enabled
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ImageSourcePropType } from "react-native";
|
|
7
7
|
import type { PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "../entities/types";
|
|
8
|
-
import type { PurchaseSource
|
|
8
|
+
import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
|
|
9
|
+
import type { PackageAllocationMap } from "../../credits/core/Credits";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Trial display configuration
|
|
@@ -2,7 +2,7 @@ import { useCallback } from "react";
|
|
|
2
2
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
3
|
import { useRestorePurchase } from "../../subscription/infrastructure/hooks/useRestorePurchase";
|
|
4
4
|
import { useAuthAwarePurchase } from "../../subscription/presentation/useAuthAwarePurchase";
|
|
5
|
-
import type { PurchaseSource } from "../../
|
|
5
|
+
import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
|
|
6
6
|
|
|
7
7
|
interface UsePaywallActionsProps {
|
|
8
8
|
source?: PurchaseSource;
|
|
@@ -92,7 +92,7 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
|
|
|
92
92
|
showAuthModal,
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
const initializeInBackground = async (userId
|
|
95
|
+
const initializeInBackground = async (userId?: string): Promise<void> => {
|
|
96
96
|
await SubscriptionManager.initialize(userId);
|
|
97
97
|
if (__DEV__) {
|
|
98
98
|
console.log('[SubscriptionInitializer] Background init complete');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CustomerInfo } from "react-native-purchases";
|
|
2
2
|
import type { RevenueCatData } from "../core/RevenueCatData";
|
|
3
|
-
import { type PeriodType, type PurchaseSource } from "../core/SubscriptionConstants";
|
|
3
|
+
import { type PeriodType, type PurchaseSource, PURCHASE_SOURCE, PURCHASE_TYPE } from "../core/SubscriptionConstants";
|
|
4
4
|
import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryProvider";
|
|
5
5
|
import { extractRevenueCatData } from "./SubscriptionSyncUtils";
|
|
6
6
|
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
@@ -19,7 +19,14 @@ export class SubscriptionSyncService {
|
|
|
19
19
|
? `purchase_${revenueCatData.originalTransactionId}`
|
|
20
20
|
: `purchase_${productId}_${Date.now()}`;
|
|
21
21
|
|
|
22
|
-
await getCreditsRepository().initializeCredits(
|
|
22
|
+
await getCreditsRepository().initializeCredits(
|
|
23
|
+
userId,
|
|
24
|
+
purchaseId,
|
|
25
|
+
productId,
|
|
26
|
+
source ?? PURCHASE_SOURCE.SETTINGS, // Default to settings if source unknown
|
|
27
|
+
revenueCatData,
|
|
28
|
+
PURCHASE_TYPE.INITIAL // Default to INITIAL
|
|
29
|
+
);
|
|
23
30
|
|
|
24
31
|
// Notify listeners via Event Bus
|
|
25
32
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
@@ -37,7 +44,14 @@ export class SubscriptionSyncService {
|
|
|
37
44
|
? `renewal_${revenueCatData.originalTransactionId}_${newExpirationDate}`
|
|
38
45
|
: `renewal_${productId}_${Date.now()}`;
|
|
39
46
|
|
|
40
|
-
await getCreditsRepository().initializeCredits(
|
|
47
|
+
await getCreditsRepository().initializeCredits(
|
|
48
|
+
userId,
|
|
49
|
+
purchaseId,
|
|
50
|
+
productId,
|
|
51
|
+
PURCHASE_SOURCE.RENEWAL,
|
|
52
|
+
revenueCatData,
|
|
53
|
+
PURCHASE_TYPE.RENEWAL
|
|
54
|
+
);
|
|
41
55
|
|
|
42
56
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
43
57
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.RENEWAL_DETECTED, { userId, productId });
|
|
@@ -61,14 +75,27 @@ export class SubscriptionSyncService {
|
|
|
61
75
|
return;
|
|
62
76
|
}
|
|
63
77
|
|
|
78
|
+
// If productId is missing, we can't initialize credits fully,
|
|
79
|
+
// but if isPremium is true, we should have it.
|
|
80
|
+
// Fallback to 'unknown' if missing, but this might throw in CreditLimitCalculator.
|
|
81
|
+
const validProductId = productId ?? 'unknown_product';
|
|
82
|
+
|
|
64
83
|
const revenueCatData: RevenueCatData = {
|
|
65
84
|
expirationDate: expiresAt ?? null,
|
|
66
85
|
willRenew: willRenew ?? false,
|
|
67
86
|
isPremium,
|
|
68
|
-
periodType
|
|
87
|
+
periodType: periodType ?? null, // Fix undefined vs null
|
|
88
|
+
originalTransactionId: null // Initialize with null as we might not have it here
|
|
69
89
|
};
|
|
70
90
|
|
|
71
|
-
await getCreditsRepository().initializeCredits(
|
|
91
|
+
await getCreditsRepository().initializeCredits(
|
|
92
|
+
userId,
|
|
93
|
+
`status_sync_${Date.now()}`,
|
|
94
|
+
validProductId,
|
|
95
|
+
PURCHASE_SOURCE.SETTINGS,
|
|
96
|
+
revenueCatData,
|
|
97
|
+
PURCHASE_TYPE.INITIAL // Status sync treated as Initial or Update
|
|
98
|
+
);
|
|
72
99
|
|
|
73
100
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
74
101
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED, { userId, isPremium });
|
|
@@ -12,7 +12,7 @@ export const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId:
|
|
|
12
12
|
willRenew: entitlement?.willRenew ?? false,
|
|
13
13
|
// Use latestPurchaseDate if originalPurchaseDate is missing, or a combine id
|
|
14
14
|
originalTransactionId: entitlement?.originalPurchaseDate || customerInfo.firstSeen,
|
|
15
|
-
periodType: entitlement?.periodType as PeriodType
|
|
15
|
+
periodType: (entitlement?.periodType as PeriodType) ?? null,
|
|
16
16
|
isPremium: !!customerInfo.entitlements.active[entitlementId],
|
|
17
17
|
};
|
|
18
18
|
};
|
|
@@ -25,21 +25,41 @@ export class PackageHandler {
|
|
|
25
25
|
|
|
26
26
|
async fetchPackages(): Promise<PurchasesPackage[]> {
|
|
27
27
|
if (!this.service.isInitialized()) {
|
|
28
|
-
throw new Error("Service not initialized");
|
|
28
|
+
throw new Error("Service not initialized. Please initialize before fetching packages.");
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
try {
|
|
32
|
+
const offering = await this.service.fetchOfferings();
|
|
33
|
+
|
|
34
|
+
if (!offering) {
|
|
35
|
+
if (__DEV__) {
|
|
36
|
+
console.warn("[PackageHandler] No offerings available from RevenueCat");
|
|
37
|
+
}
|
|
38
|
+
// Return empty array instead of throwing - allows graceful degradation
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const packages = offering.availablePackages;
|
|
43
|
+
if (!packages || packages.length === 0) {
|
|
44
|
+
if (__DEV__) {
|
|
45
|
+
console.warn("[PackageHandler] No packages available in offering");
|
|
46
|
+
}
|
|
47
|
+
// Return empty array instead of throwing - allows graceful degradation
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return packages;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (__DEV__) {
|
|
54
|
+
console.error("[PackageHandler] Failed to fetch packages:", error);
|
|
55
|
+
}
|
|
56
|
+
// Re-throw with more context
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Failed to fetch subscription packages. ${
|
|
59
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
60
|
+
}`
|
|
61
|
+
);
|
|
35
62
|
}
|
|
36
|
-
|
|
37
|
-
const packages = offering.availablePackages;
|
|
38
|
-
if (!packages) {
|
|
39
|
-
throw new Error("No packages available in offering");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return packages;
|
|
43
63
|
}
|
|
44
64
|
|
|
45
65
|
async purchase(pkg: PurchasesPackage, userId: string): Promise<boolean> {
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* TanStack Query keys and constants for subscription state
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/** Query cache time constants */
|
|
7
|
+
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* Query keys for TanStack Query
|
|
8
11
|
*/
|
|
@@ -12,7 +15,4 @@ export const SUBSCRIPTION_QUERY_KEYS = {
|
|
|
12
15
|
["subscription", "initialized", userId] as const,
|
|
13
16
|
} as const;
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
// This ensures users always see real-time subscription status
|
|
17
|
-
export const STALE_TIME = 0; // Always stale - refetch immediately
|
|
18
|
-
export const GC_TIME = 0; // Don't cache - garbage collect immediately
|
|
18
|
+
|
|
@@ -12,8 +12,6 @@ import {
|
|
|
12
12
|
import { SubscriptionManager } from '../../infrastructure/managers/SubscriptionManager';
|
|
13
13
|
import {
|
|
14
14
|
SUBSCRIPTION_QUERY_KEYS,
|
|
15
|
-
STALE_TIME,
|
|
16
|
-
GC_TIME,
|
|
17
15
|
} from "./subscriptionQueryKeys";
|
|
18
16
|
|
|
19
17
|
/**
|
|
@@ -41,11 +39,7 @@ export const useSubscriptionPackages = () => {
|
|
|
41
39
|
|
|
42
40
|
return SubscriptionManager.getPackages();
|
|
43
41
|
},
|
|
44
|
-
staleTime: STALE_TIME,
|
|
45
|
-
gcTime: GC_TIME,
|
|
46
42
|
enabled: isConfigured,
|
|
47
|
-
|
|
48
|
-
refetchOnWindowFocus: true, // Refetch when app becomes active
|
|
49
|
-
refetchOnReconnect: true, // Refetch when network reconnects
|
|
43
|
+
|
|
50
44
|
});
|
|
51
45
|
};
|
|
@@ -53,10 +53,11 @@ class SubscriptionManagerImpl {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
async initialize(userId
|
|
56
|
+
async initialize(userId?: string): Promise<boolean> {
|
|
57
57
|
this.ensureConfigured();
|
|
58
58
|
|
|
59
|
-
const
|
|
59
|
+
const actualUserId = userId ?? (await this.managerConfig!.getAnonymousUserId());
|
|
60
|
+
const { shouldInit, existingPromise } = this.state.initCache.tryAcquireInitialization(actualUserId);
|
|
60
61
|
|
|
61
62
|
if (!shouldInit && existingPromise) {
|
|
62
63
|
return existingPromise;
|
|
@@ -71,11 +72,11 @@ class SubscriptionManagerImpl {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
this.ensurePackageHandlerInitialized();
|
|
74
|
-
const result = await this.serviceInstance.initialize(
|
|
75
|
+
const result = await this.serviceInstance.initialize(actualUserId);
|
|
75
76
|
return result.success;
|
|
76
77
|
})();
|
|
77
78
|
|
|
78
|
-
this.state.initCache.setPromise(promise,
|
|
79
|
+
this.state.initCache.setPromise(promise, actualUserId);
|
|
79
80
|
return promise;
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -11,6 +11,8 @@ export class InitializationCache {
|
|
|
11
11
|
private initializationInProgress = false;
|
|
12
12
|
// Track which userId the promise is for (separate from currentUserId which is set after completion)
|
|
13
13
|
private promiseUserId: string | null = null;
|
|
14
|
+
// Track promise completion state to avoid returning failed promises
|
|
15
|
+
private promiseCompleted = true;
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Atomically check if reinitialization is needed AND reserve the slot
|
|
@@ -22,17 +24,24 @@ export class InitializationCache {
|
|
|
22
24
|
return { shouldInit: false, existingPromise: this.initPromise };
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
// If already initialized for this user and promise
|
|
26
|
-
if
|
|
27
|
+
// If already initialized for this user and promise completed successfully, return it
|
|
28
|
+
// Only return cached promise if it completed AND it's for the same user
|
|
29
|
+
if (this.initPromise && this.currentUserId === userId && !this.initializationInProgress && this.promiseCompleted) {
|
|
27
30
|
return { shouldInit: false, existingPromise: this.initPromise };
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
// Different user
|
|
31
|
-
// Atomically set the flag
|
|
32
|
-
this.initializationInProgress
|
|
33
|
-
|
|
33
|
+
// Different user, no initialization, or failed promise - need to reinitialize
|
|
34
|
+
// Atomically set the flag and clear previous state if needed
|
|
35
|
+
if (!this.initializationInProgress) {
|
|
36
|
+
this.initializationInProgress = true;
|
|
37
|
+
this.promiseUserId = userId;
|
|
38
|
+
this.promiseCompleted = false;
|
|
39
|
+
return { shouldInit: true, existingPromise: null };
|
|
40
|
+
}
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
// If we reach here, initialization is in progress for a different user
|
|
43
|
+
// Wait for current initialization to complete
|
|
44
|
+
return { shouldInit: false, existingPromise: this.initPromise };
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
setPromise(promise: Promise<boolean>, userId: string): void {
|
|
@@ -45,6 +54,7 @@ export class InitializationCache {
|
|
|
45
54
|
if (result && this.promiseUserId === userId) {
|
|
46
55
|
this.currentUserId = userId;
|
|
47
56
|
}
|
|
57
|
+
this.promiseCompleted = true;
|
|
48
58
|
return result;
|
|
49
59
|
})
|
|
50
60
|
.catch(() => {
|
|
@@ -52,7 +62,9 @@ export class InitializationCache {
|
|
|
52
62
|
if (this.promiseUserId === userId) {
|
|
53
63
|
this.initPromise = null;
|
|
54
64
|
this.promiseUserId = null;
|
|
65
|
+
this.currentUserId = null; // Clear user on failure
|
|
55
66
|
}
|
|
67
|
+
this.promiseCompleted = true;
|
|
56
68
|
})
|
|
57
69
|
.finally(() => {
|
|
58
70
|
// Always release the mutex
|
|
@@ -71,5 +83,6 @@ export class InitializationCache {
|
|
|
71
83
|
this.currentUserId = null;
|
|
72
84
|
this.initializationInProgress = false;
|
|
73
85
|
this.promiseUserId = null;
|
|
86
|
+
this.promiseCompleted = true;
|
|
74
87
|
}
|
|
75
88
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { CustomerInfo } from "react-native-purchases";
|
|
7
7
|
import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
|
|
8
|
-
import type { PurchaseSource } from "../../../
|
|
8
|
+
import type { PurchaseSource } from "../../../subscription/core/SubscriptionConstants";
|
|
9
9
|
import { getPremiumEntitlement } from "../../core/RevenueCatTypes";
|
|
10
10
|
|
|
11
11
|
export async function syncPremiumStatus(
|
|
@@ -11,22 +11,61 @@ import {
|
|
|
11
11
|
ScreenLayout,
|
|
12
12
|
} from "@umituz/react-native-design-system";
|
|
13
13
|
import { SubscriptionHeader } from "./components/SubscriptionHeader";
|
|
14
|
-
import { CreditsList } from "./components/CreditsList";
|
|
15
|
-
import { UpgradePrompt } from "./components/UpgradePrompt";
|
|
16
|
-
import { DevTestSection } from "./components/DevTestSection";
|
|
17
|
-
import type { SubscriptionDetailScreenProps } from "../types/SubscriptionDetailTypes";
|
|
14
|
+
import { CreditsList, type CreditItem } from "./components/CreditsList";
|
|
15
|
+
import { UpgradePrompt, type Benefit } from "./components/UpgradePrompt";
|
|
16
|
+
import { DevTestSection, type DevTestActions } from "./components/DevTestSection";
|
|
18
17
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
export interface SubscriptionDisplayFlags {
|
|
19
|
+
showHeader: boolean;
|
|
20
|
+
showCredits: boolean;
|
|
21
|
+
showUpgradePrompt: boolean;
|
|
22
|
+
showExpirationDate: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SubscriptionDetailTranslations {
|
|
26
|
+
title: string;
|
|
27
|
+
statusActive: string;
|
|
28
|
+
statusExpired: string;
|
|
29
|
+
statusFree: string;
|
|
30
|
+
statusCanceled: string;
|
|
31
|
+
statusLabel: string;
|
|
32
|
+
lifetimeLabel: string;
|
|
33
|
+
expiresLabel: string;
|
|
34
|
+
purchasedLabel: string;
|
|
35
|
+
usageTitle?: string;
|
|
36
|
+
creditsTitle: string;
|
|
37
|
+
creditsResetInfo?: string;
|
|
38
|
+
remainingLabel?: string;
|
|
39
|
+
upgradeButton: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DevToolsConfig {
|
|
43
|
+
actions: DevTestActions;
|
|
44
|
+
title?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface UpgradePromptConfig {
|
|
48
|
+
title: string;
|
|
49
|
+
subtitle?: string;
|
|
50
|
+
benefits?: readonly Benefit[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SubscriptionDetailConfig {
|
|
54
|
+
display: SubscriptionDisplayFlags;
|
|
55
|
+
statusType: "active" | "expired" | "none" | "canceled";
|
|
56
|
+
isLifetime: boolean;
|
|
57
|
+
expirationDate?: string;
|
|
58
|
+
purchaseDate?: string;
|
|
59
|
+
daysRemaining?: number | null;
|
|
60
|
+
credits?: readonly CreditItem[];
|
|
61
|
+
translations: SubscriptionDetailTranslations;
|
|
62
|
+
upgradePrompt?: UpgradePromptConfig & { onUpgrade?: () => void };
|
|
63
|
+
devTools?: DevToolsConfig;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SubscriptionDetailScreenProps {
|
|
67
|
+
config: SubscriptionDetailConfig;
|
|
68
|
+
}
|
|
30
69
|
|
|
31
70
|
export const SubscriptionDetailScreen: React.FC<
|
|
32
71
|
SubscriptionDetailScreenProps
|
|
@@ -98,7 +137,7 @@ export const SubscriptionDetailScreen: React.FC<
|
|
|
98
137
|
subtitle={config.upgradePrompt.subtitle}
|
|
99
138
|
benefits={config.upgradePrompt.benefits}
|
|
100
139
|
upgradeButtonLabel={config.translations.upgradeButton}
|
|
101
|
-
onUpgrade={config.onUpgrade}
|
|
140
|
+
onUpgrade={config.upgradePrompt.onUpgrade ?? (() => {})}
|
|
102
141
|
/>
|
|
103
142
|
)}
|
|
104
143
|
</View>
|
|
@@ -7,7 +7,20 @@ import React, { useMemo } from "react";
|
|
|
7
7
|
import { View, StyleSheet } from "react-native";
|
|
8
8
|
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
9
9
|
import { CreditRow } from "../../components/details/CreditRow";
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
export interface CreditItem {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
current: number;
|
|
15
|
+
total: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CreditsListProps {
|
|
19
|
+
credits: readonly CreditItem[];
|
|
20
|
+
title?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
remainingLabel?: string;
|
|
23
|
+
}
|
|
11
24
|
|
|
12
25
|
export const CreditsList: React.FC<CreditsListProps> = ({
|
|
13
26
|
credits,
|
|
@@ -7,9 +7,17 @@
|
|
|
7
7
|
import React, { useMemo } from "react";
|
|
8
8
|
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
9
9
|
import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
10
|
-
import type { DevTestSectionProps } from "../../types/SubscriptionDetailTypes";
|
|
11
10
|
|
|
12
|
-
export
|
|
11
|
+
export interface DevTestActions {
|
|
12
|
+
onTestRenewal: () => void;
|
|
13
|
+
onCheckCredits: () => void;
|
|
14
|
+
onTestDuplicate: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DevTestSectionProps {
|
|
18
|
+
actions: DevTestActions;
|
|
19
|
+
title?: string;
|
|
20
|
+
}
|
|
13
21
|
|
|
14
22
|
/** Dev test button translations */
|
|
15
23
|
export interface DevTestTranslations {
|
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
import React, { useMemo } from "react";
|
|
7
7
|
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
8
|
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
export interface SubscriptionActionsProps {
|
|
11
|
+
isPremium: boolean;
|
|
12
|
+
upgradeButtonLabel?: string;
|
|
13
|
+
onUpgrade?: () => void;
|
|
14
|
+
}
|
|
10
15
|
|
|
11
16
|
export const SubscriptionActions: React.FC<SubscriptionActionsProps> = ({
|
|
12
17
|
isPremium,
|
|
@@ -8,7 +8,26 @@ import { View, StyleSheet } from "react-native";
|
|
|
8
8
|
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
9
9
|
import { PremiumStatusBadge } from "../../components/details/PremiumStatusBadge";
|
|
10
10
|
import { DetailRow } from "../../components/details/DetailRow";
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
export interface SubscriptionHeaderProps {
|
|
13
|
+
statusType: "active" | "expired" | "none" | "canceled";
|
|
14
|
+
showExpirationDate: boolean;
|
|
15
|
+
isLifetime: boolean;
|
|
16
|
+
expirationDate?: string;
|
|
17
|
+
purchaseDate?: string;
|
|
18
|
+
daysRemaining?: number | null;
|
|
19
|
+
translations: {
|
|
20
|
+
title: string;
|
|
21
|
+
statusActive: string;
|
|
22
|
+
statusExpired: string;
|
|
23
|
+
statusFree: string;
|
|
24
|
+
statusCanceled: string;
|
|
25
|
+
statusLabel: string;
|
|
26
|
+
lifetimeLabel: string;
|
|
27
|
+
expiresLabel: string;
|
|
28
|
+
purchasedLabel: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
12
31
|
|
|
13
32
|
export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
|
|
14
33
|
statusType,
|
|
@@ -10,7 +10,19 @@ import {
|
|
|
10
10
|
AtomicText,
|
|
11
11
|
AtomicIcon,
|
|
12
12
|
} from "@umituz/react-native-design-system";
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
export interface Benefit {
|
|
15
|
+
icon?: string;
|
|
16
|
+
text: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UpgradePromptProps {
|
|
20
|
+
title: string;
|
|
21
|
+
subtitle?: string;
|
|
22
|
+
benefits?: readonly Benefit[];
|
|
23
|
+
upgradeButtonLabel: string;
|
|
24
|
+
onUpgrade: () => void;
|
|
25
|
+
}
|
|
14
26
|
|
|
15
27
|
export const UpgradePrompt: React.FC<UpgradePromptProps> = ({
|
|
16
28
|
title,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { useCallback } from "react";
|
|
7
7
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
8
8
|
import { usePremium } from "./usePremium";
|
|
9
|
-
import type { PurchaseSource } from "
|
|
9
|
+
import type { PurchaseSource } from "../core/SubscriptionConstants";
|
|
10
10
|
|
|
11
11
|
export interface PurchaseAuthProvider {
|
|
12
12
|
isAuthenticated: () => boolean;
|