@umituz/react-native-subscription 2.37.38 → 2.37.40
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/CreditsInitializer.ts +5 -20
- package/src/domains/credits/application/DeductCreditsCommand.ts +13 -6
- package/src/domains/credits/application/RefundCreditsCommand.ts +1 -5
- package/src/domains/credits/application/credit-strategies/CreditAllocationOrchestrator.ts +1 -9
- package/src/domains/credits/application/credit-strategies/ICreditStrategy.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/application/creditOperationUtils.ts +1 -43
- package/src/domains/credits/core/Credits.ts +0 -23
- package/src/domains/credits/core/CreditsConstants.ts +0 -11
- package/src/domains/credits/core/CreditsMapper.ts +0 -6
- package/src/domains/credits/core/UserCreditsDocument.ts +0 -12
- package/src/domains/credits/infrastructure/CreditsRepository.ts +6 -1
- package/src/domains/credits/infrastructure/CreditsRepositoryManager.ts +0 -21
- package/src/domains/credits/infrastructure/operations/CreditsWriter.ts +52 -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 +0 -4
- package/src/domains/subscription/application/SubscriptionSyncService.ts +4 -8
- 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/application/statusChangeHandlers.ts +14 -27
- package/src/domains/subscription/application/syncIdGenerators.ts +0 -4
- 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 +3 -17
- package/src/domains/subscription/infrastructure/utils/authPurchaseState.ts +0 -5
- 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 +30 -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/credits/application/credit-strategies/SyncCreditStrategy.ts +0 -24
- 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
|
@@ -20,6 +20,14 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
const attemptInitWithRetry = async (revenueCatUserId?: string, attempt = 0): Promise<void> => {
|
|
23
|
+
// Abort if user changed since retry was scheduled
|
|
24
|
+
if (attempt > 0 && lastUserId !== revenueCatUserId) {
|
|
25
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
26
|
+
console.log('[BackgroundInitializer] Aborting retry - user changed');
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
try {
|
|
24
32
|
await initializeInBackground(revenueCatUserId);
|
|
25
33
|
lastUserId = revenueCatUserId;
|
|
@@ -49,7 +57,6 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
|
|
|
49
57
|
};
|
|
50
58
|
|
|
51
59
|
const debouncedInitialize = (revenueCatUserId?: string): void => {
|
|
52
|
-
// Clear any pending initialization or retry
|
|
53
60
|
if (debounceTimer) {
|
|
54
61
|
clearTimeout(debounceTimer);
|
|
55
62
|
}
|
|
@@ -58,7 +65,6 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
|
|
|
58
65
|
retryTimer = null;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
// If userId hasn't changed AND last init succeeded, skip
|
|
62
68
|
if (lastUserId === revenueCatUserId && lastInitSucceeded) {
|
|
63
69
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
64
70
|
console.log('[BackgroundInitializer] UserId unchanged and init succeeded, skipping');
|
|
@@ -70,6 +76,13 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
|
|
|
70
76
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
71
77
|
console.log('[BackgroundInitializer] Auth state listener triggered, reinitializing with userId:', revenueCatUserId || '(undefined - anonymous)');
|
|
72
78
|
}
|
|
79
|
+
|
|
80
|
+
// Reset subscription state on logout to prevent stale cache
|
|
81
|
+
if (!revenueCatUserId && lastUserId) {
|
|
82
|
+
await SubscriptionManager.reset();
|
|
83
|
+
lastInitSucceeded = false;
|
|
84
|
+
}
|
|
85
|
+
|
|
73
86
|
void attemptInitWithRetry(revenueCatUserId);
|
|
74
87
|
}, AUTH_STATE_DEBOUNCE_MS);
|
|
75
88
|
};
|
|
@@ -5,6 +5,7 @@ import { SubscriptionSyncService } from "../SubscriptionSyncService";
|
|
|
5
5
|
import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
|
|
6
6
|
import type { CustomerInfo } from "react-native-purchases";
|
|
7
7
|
import type { PackageType } from "../../../revenuecat/core/types/RevenueCatTypes";
|
|
8
|
+
import { PURCHASE_SOURCE, PERIOD_TYPE, type PurchaseSource, type PeriodType } from "../../core/SubscriptionConstants";
|
|
8
9
|
|
|
9
10
|
export function configureServices(config: SubscriptionInitConfig, apiKey: string): SubscriptionSyncService {
|
|
10
11
|
const { entitlementId, credits, creditPackages, getFirebaseAuth, showAuthModal, onCreditsUpdated, getAnonymousUserId } = config;
|
|
@@ -31,7 +32,10 @@ export function configureServices(config: SubscriptionInitConfig, apiKey: string
|
|
|
31
32
|
c: CustomerInfo,
|
|
32
33
|
s?: string,
|
|
33
34
|
pkgType?: PackageType | null
|
|
34
|
-
) =>
|
|
35
|
+
) => {
|
|
36
|
+
const validSource = s && Object.values(PURCHASE_SOURCE).includes(s as PurchaseSource) ? s as PurchaseSource : undefined;
|
|
37
|
+
return syncService.handlePurchase(u, p, c, validSource, pkgType);
|
|
38
|
+
},
|
|
35
39
|
onRenewalDetected: (
|
|
36
40
|
u: string,
|
|
37
41
|
p: string,
|
|
@@ -45,7 +49,10 @@ export function configureServices(config: SubscriptionInitConfig, apiKey: string
|
|
|
45
49
|
exp?: string,
|
|
46
50
|
willR?: boolean,
|
|
47
51
|
pt?: string
|
|
48
|
-
) =>
|
|
52
|
+
) => {
|
|
53
|
+
const validPeriodType = pt && Object.values(PERIOD_TYPE).includes(pt as PeriodType) ? pt as PeriodType : undefined;
|
|
54
|
+
return syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, validPeriodType);
|
|
55
|
+
},
|
|
49
56
|
onCreditsUpdated,
|
|
50
57
|
},
|
|
51
58
|
apiKey,
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import type { RevenueCatData } from "../../revenuecat/core/types";
|
|
2
1
|
import type { PeriodType } from "../core/SubscriptionConstants";
|
|
3
|
-
import { PURCHASE_SOURCE, PURCHASE_TYPE } from "../core/SubscriptionConstants";
|
|
4
2
|
import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
|
|
5
3
|
import { emitCreditsUpdated } from "./syncEventEmitter";
|
|
6
|
-
import { generateStatusSyncId } from "./syncIdGenerators";
|
|
7
4
|
|
|
8
5
|
export const handleExpiredSubscription = async (userId: string): Promise<void> => {
|
|
9
6
|
await getCreditsRepository().syncExpiredStatus(userId);
|
|
@@ -16,32 +13,22 @@ export const handlePremiumStatusSync = async (
|
|
|
16
13
|
productId: string,
|
|
17
14
|
expiresAt: string | null,
|
|
18
15
|
willRenew: boolean,
|
|
19
|
-
periodType: PeriodType | null
|
|
16
|
+
periodType: PeriodType | null,
|
|
17
|
+
unsubscribeDetectedAt?: string | null,
|
|
18
|
+
billingIssueDetectedAt?: string | null,
|
|
19
|
+
store?: string | null,
|
|
20
|
+
ownershipType?: string | null
|
|
20
21
|
): Promise<void> => {
|
|
21
|
-
|
|
22
|
-
const revenueCatData: RevenueCatData = {
|
|
23
|
-
expirationDate: expiresAt,
|
|
24
|
-
willRenew,
|
|
22
|
+
await getCreditsRepository().syncPremiumMetadata(userId, {
|
|
25
23
|
isPremium,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
originalTransactionId: null,
|
|
29
|
-
unsubscribeDetectedAt: null,
|
|
30
|
-
billingIssueDetectedAt: null,
|
|
31
|
-
store: null,
|
|
32
|
-
ownershipType: null
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const statusSyncId = generateStatusSyncId(userId, isPremium);
|
|
36
|
-
|
|
37
|
-
await getCreditsRepository().initializeCredits(
|
|
38
|
-
userId,
|
|
39
|
-
statusSyncId,
|
|
24
|
+
willRenew,
|
|
25
|
+
expirationDate: expiresAt,
|
|
40
26
|
productId,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
27
|
+
periodType,
|
|
28
|
+
unsubscribeDetectedAt: unsubscribeDetectedAt ?? null,
|
|
29
|
+
billingIssueDetectedAt: billingIssueDetectedAt ?? null,
|
|
30
|
+
store: store ?? null,
|
|
31
|
+
ownershipType: ownershipType ?? null,
|
|
32
|
+
});
|
|
46
33
|
emitCreditsUpdated(userId);
|
|
47
34
|
};
|
|
@@ -9,7 +9,3 @@ export const generateRenewalId = (originalTransactionId: string | null, productI
|
|
|
9
9
|
? `renewal_${originalTransactionId}_${expirationDate}`
|
|
10
10
|
: `renewal_${productId}_${Date.now()}`;
|
|
11
11
|
};
|
|
12
|
-
|
|
13
|
-
export const generateStatusSyncId = (userId: string, isPremium: boolean): string => {
|
|
14
|
-
return `status_sync_${userId}_${isPremium ? 'premium' : 'free'}`;
|
|
15
|
-
};
|
|
@@ -1,10 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription Threshold Constants
|
|
3
|
-
* Centralized threshold values for subscription UI logic
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Number of days before expiration to show warnings
|
|
8
|
-
* Used across subscription UI components for consistency
|
|
9
|
-
*/
|
|
10
1
|
export const EXPIRATION_WARNING_THRESHOLD_DAYS = 7;
|
|
@@ -6,8 +6,6 @@ export const USER_TIER = {
|
|
|
6
6
|
|
|
7
7
|
export type UserTierType = (typeof USER_TIER)[keyof typeof USER_TIER];
|
|
8
8
|
|
|
9
|
-
export const DEFAULT_ENTITLEMENT_ID = 'premium';
|
|
10
|
-
|
|
11
9
|
export const SUBSCRIPTION_STATUS = {
|
|
12
10
|
ACTIVE: 'active',
|
|
13
11
|
TRIAL: 'trial',
|
|
@@ -63,5 +61,3 @@ export const PURCHASE_TYPE = {
|
|
|
63
61
|
} as const;
|
|
64
62
|
|
|
65
63
|
export type PurchaseType = (typeof PURCHASE_TYPE)[keyof typeof PURCHASE_TYPE];
|
|
66
|
-
|
|
67
|
-
|
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import { timezoneService } from "@umituz/react-native-design-system";
|
|
2
|
-
import {
|
|
3
|
-
SUBSCRIPTION_STATUS,
|
|
4
|
-
|
|
5
|
-
type PeriodType,
|
|
6
|
-
type SubscriptionStatusType
|
|
2
|
+
import {
|
|
3
|
+
SUBSCRIPTION_STATUS,
|
|
4
|
+
type SubscriptionStatusType
|
|
7
5
|
} from "./SubscriptionConstants";
|
|
8
|
-
import {
|
|
9
|
-
InactiveStatusHandler,
|
|
10
|
-
TrialStatusHandler,
|
|
11
|
-
ActiveStatusHandler
|
|
6
|
+
import {
|
|
7
|
+
InactiveStatusHandler,
|
|
8
|
+
TrialStatusHandler,
|
|
9
|
+
ActiveStatusHandler
|
|
12
10
|
} from "./SubscriptionStatusHandlers";
|
|
13
11
|
|
|
14
|
-
export {
|
|
15
|
-
PERIOD_TYPE,
|
|
16
|
-
type PeriodType,
|
|
17
|
-
type SubscriptionStatusType
|
|
18
|
-
};
|
|
12
|
+
export type { SubscriptionStatusType };
|
|
19
13
|
|
|
20
14
|
export interface SubscriptionStatus {
|
|
21
15
|
isPremium: boolean;
|
|
@@ -25,7 +19,7 @@ export interface SubscriptionStatus {
|
|
|
25
19
|
customerId?: string | null;
|
|
26
20
|
syncedAt?: string | null;
|
|
27
21
|
status?: SubscriptionStatusType;
|
|
28
|
-
periodType?: string;
|
|
22
|
+
periodType?: string;
|
|
29
23
|
isTrialing?: boolean;
|
|
30
24
|
}
|
|
31
25
|
|
|
@@ -41,7 +35,7 @@ export const createDefaultSubscriptionStatus = (): SubscriptionStatus => ({
|
|
|
41
35
|
|
|
42
36
|
export const isSubscriptionValid = (status: SubscriptionStatus | null): boolean => {
|
|
43
37
|
if (!status || !status.isPremium) return false;
|
|
44
|
-
if (!status.expiresAt) return true;
|
|
38
|
+
if (!status.expiresAt) return true;
|
|
45
39
|
return timezoneService.isFuture(new Date(status.expiresAt));
|
|
46
40
|
};
|
|
47
41
|
|
|
@@ -49,18 +43,14 @@ export interface StatusResolverInput {
|
|
|
49
43
|
isPremium: boolean;
|
|
50
44
|
willRenew?: boolean;
|
|
51
45
|
isExpired?: boolean;
|
|
52
|
-
periodType?: string;
|
|
46
|
+
periodType?: string;
|
|
53
47
|
}
|
|
54
48
|
|
|
55
|
-
// Singleton Chain Instance
|
|
56
49
|
const inactiveHandler = new InactiveStatusHandler();
|
|
57
50
|
inactiveHandler
|
|
58
51
|
.setNext(new TrialStatusHandler())
|
|
59
52
|
.setNext(new ActiveStatusHandler());
|
|
60
53
|
|
|
61
|
-
/**
|
|
62
|
-
* Resolves subscription status using Chain of Responsibility Pattern.
|
|
63
|
-
*/
|
|
64
54
|
export const resolveSubscriptionStatus = (input: StatusResolverInput): SubscriptionStatusType => {
|
|
65
55
|
return inactiveHandler.handle(input);
|
|
66
56
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SUBSCRIPTION_STATUS,
|
|
3
|
-
PERIOD_TYPE,
|
|
4
|
-
type SubscriptionStatusType
|
|
1
|
+
import {
|
|
2
|
+
SUBSCRIPTION_STATUS,
|
|
3
|
+
PERIOD_TYPE,
|
|
4
|
+
type SubscriptionStatusType
|
|
5
5
|
} from "./SubscriptionConstants";
|
|
6
6
|
import type { StatusResolverInput } from "./SubscriptionStatus";
|
|
7
7
|
|
|
@@ -20,7 +20,6 @@ abstract class BaseStatusHandler {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/** Handles Expired or No-Premium cases */
|
|
24
23
|
export class InactiveStatusHandler extends BaseStatusHandler {
|
|
25
24
|
handle(input: StatusResolverInput): SubscriptionStatusType {
|
|
26
25
|
const isExpired = input.isExpired === true;
|
|
@@ -32,7 +31,6 @@ export class InactiveStatusHandler extends BaseStatusHandler {
|
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
/** Handles Trial-related states */
|
|
36
34
|
export class TrialStatusHandler extends BaseStatusHandler {
|
|
37
35
|
handle(input: StatusResolverInput): SubscriptionStatusType {
|
|
38
36
|
if (input.periodType === PERIOD_TYPE.TRIAL) {
|
|
@@ -42,7 +40,6 @@ export class TrialStatusHandler extends BaseStatusHandler {
|
|
|
42
40
|
}
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
/** Handles Canceled-Active states */
|
|
46
43
|
export class ActiveStatusHandler extends BaseStatusHandler {
|
|
47
44
|
handle(input: StatusResolverInput): SubscriptionStatusType {
|
|
48
45
|
if (input.willRenew === false) {
|
|
@@ -38,7 +38,7 @@ export class PurchaseStatusResolver {
|
|
|
38
38
|
isSandbox: entitlement.isSandbox ?? false,
|
|
39
39
|
periodType: entitlement.periodType ?? null,
|
|
40
40
|
packageType: detectedPackageType,
|
|
41
|
-
store: null,
|
|
41
|
+
store: entitlement.store ?? null,
|
|
42
42
|
gracePeriodExpiresDate: null,
|
|
43
43
|
unsubscribeDetectedAt: toDate(entitlement.unsubscribeDetectedAt),
|
|
44
44
|
};
|
|
@@ -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
|
}
|