@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.
Files changed (141) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/application/CreditLimitCalculator.ts +1 -9
  3. package/src/domains/credits/application/CreditsInitializer.ts +5 -20
  4. package/src/domains/credits/application/DeductCreditsCommand.ts +13 -6
  5. package/src/domains/credits/application/RefundCreditsCommand.ts +1 -5
  6. package/src/domains/credits/application/credit-strategies/CreditAllocationOrchestrator.ts +1 -9
  7. package/src/domains/credits/application/credit-strategies/ICreditStrategy.ts +1 -5
  8. package/src/domains/credits/application/credit-strategies/TrialCreditStrategy.ts +1 -5
  9. package/src/domains/credits/application/creditDocumentHelpers.ts +2 -9
  10. package/src/domains/credits/application/creditOperationUtils.ts +1 -43
  11. package/src/domains/credits/core/Credits.ts +0 -23
  12. package/src/domains/credits/core/CreditsConstants.ts +0 -11
  13. package/src/domains/credits/core/CreditsMapper.ts +0 -6
  14. package/src/domains/credits/core/UserCreditsDocument.ts +0 -12
  15. package/src/domains/credits/infrastructure/CreditsRepository.ts +6 -1
  16. package/src/domains/credits/infrastructure/CreditsRepositoryManager.ts +0 -21
  17. package/src/domains/credits/infrastructure/operations/CreditsWriter.ts +52 -1
  18. package/src/domains/credits/presentation/deduct-credit/useDeductCredit.ts +2 -2
  19. package/src/domains/credits/presentation/useCredits.ts +10 -9
  20. package/src/domains/paywall/components/PaywallContainer.types.ts +0 -28
  21. package/src/domains/paywall/components/PaywallModal.styles.ts +0 -4
  22. package/src/domains/paywall/entities/types.ts +0 -5
  23. package/src/domains/paywall/hooks/usePaywallActions.ts +1 -15
  24. package/src/domains/revenuecat/core/errors/RevenueCatError.ts +0 -6
  25. package/src/domains/revenuecat/core/errors/RevenueCatErrorHandler.ts +0 -24
  26. package/src/domains/revenuecat/core/errors/RevenueCatErrorMessages.ts +0 -18
  27. package/src/domains/revenuecat/core/errors/index.ts +0 -4
  28. package/src/domains/revenuecat/core/types/RevenueCatConfig.ts +3 -7
  29. package/src/domains/revenuecat/core/types/RevenueCatData.ts +4 -9
  30. package/src/domains/revenuecat/core/types/RevenueCatTypes.ts +5 -65
  31. package/src/domains/revenuecat/core/types/index.ts +0 -4
  32. package/src/domains/revenuecat/infrastructure/services/UserSwitchMutex.ts +1 -24
  33. package/src/domains/subscription/application/SubscriptionAuthListener.ts +5 -21
  34. package/src/domains/subscription/application/SubscriptionInitializerTypes.ts +1 -5
  35. package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +0 -4
  36. package/src/domains/subscription/application/SubscriptionSyncService.ts +4 -8
  37. package/src/domains/subscription/application/SubscriptionSyncUtils.ts +1 -1
  38. package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +15 -2
  39. package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +9 -2
  40. package/src/domains/subscription/application/statusChangeHandlers.ts +14 -27
  41. package/src/domains/subscription/application/syncIdGenerators.ts +0 -4
  42. package/src/domains/subscription/constants/thresholds.ts +0 -9
  43. package/src/domains/subscription/core/SubscriptionConstants.ts +0 -4
  44. package/src/domains/subscription/core/SubscriptionStatus.ts +11 -21
  45. package/src/domains/subscription/core/SubscriptionStatusHandlers.ts +4 -7
  46. package/src/domains/subscription/infrastructure/handlers/PurchaseStatusResolver.ts +1 -1
  47. package/src/domains/subscription/infrastructure/hooks/subscriptionQueryKeys.ts +0 -13
  48. package/src/domains/subscription/infrastructure/hooks/usePurchasePackage.ts +0 -18
  49. package/src/domains/subscription/infrastructure/hooks/useRestorePurchase.ts +3 -17
  50. package/src/domains/subscription/infrastructure/hooks/useRevenueCatTrialEligibility.ts +0 -17
  51. package/src/domains/subscription/infrastructure/hooks/useSubscriptionPackages.ts +0 -19
  52. package/src/domains/subscription/infrastructure/hooks/useSubscriptionQueries.ts +0 -6
  53. package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +0 -17
  54. package/src/domains/subscription/infrastructure/state/initializationState.ts +0 -25
  55. package/src/domains/subscription/infrastructure/utils/InitializationCache.ts +0 -21
  56. package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +3 -17
  57. package/src/domains/subscription/infrastructure/utils/authPurchaseState.ts +0 -5
  58. package/src/domains/subscription/infrastructure/utils/renewal/PackageTierComparator.ts +1 -0
  59. package/src/domains/subscription/infrastructure/utils/trialEligibilityUtils.ts +0 -18
  60. package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.styles.ts +0 -5
  61. package/src/domains/subscription/presentation/components/details/PremiumDetailsCardTypes.ts +0 -5
  62. package/src/domains/subscription/presentation/components/feedback/paywallFeedbackStyles.ts +0 -5
  63. package/src/domains/subscription/presentation/stores/index.ts +0 -4
  64. package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +0 -13
  65. package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +30 -21
  66. package/src/domains/subscription/presentation/usePaywallVisibility.ts +0 -9
  67. package/src/domains/subscription/presentation/useSubscriptionStatus.ts +8 -11
  68. package/src/domains/subscription/utils/authGuards.ts +3 -0
  69. package/src/domains/trial/application/TrialService.ts +0 -9
  70. package/src/domains/trial/core/TrialTypes.ts +0 -8
  71. package/src/domains/wallet/domain/mappers/TransactionMapper.ts +0 -5
  72. package/src/domains/wallet/domain/types/transaction.types.ts +0 -7
  73. package/src/domains/wallet/index.ts +0 -7
  74. package/src/domains/wallet/infrastructure/config/walletConfig.ts +0 -11
  75. package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +6 -3
  76. package/src/domains/wallet/presentation/hooks/useWallet.ts +0 -7
  77. package/src/domains/wallet/utils/transactionIconMap.ts +0 -10
  78. package/src/global.d.ts +0 -6
  79. package/src/index.ts +1 -4
  80. package/src/init/createSubscriptionInitModule.ts +12 -2
  81. package/src/init/index.ts +1 -5
  82. package/src/presentation/hooks/feedback/useFeedbackSubmit.ts +0 -11
  83. package/src/shared/application/FeedbackService.ts +3 -21
  84. package/src/shared/application/ports/ISubscriptionRepository.ts +0 -4
  85. package/src/shared/infrastructure/SubscriptionEventBus.ts +0 -13
  86. package/src/shared/infrastructure/firestore/collectionUtils.ts +1 -17
  87. package/src/shared/infrastructure/firestore/index.ts +0 -4
  88. package/src/shared/infrastructure/firestore/resultUtils.ts +0 -12
  89. package/src/shared/infrastructure/react-query/hooks/usePreviousUserCleanup.ts +0 -17
  90. package/src/shared/infrastructure/react-query/queryConfig.ts +0 -15
  91. package/src/shared/utils/BaseError.ts +0 -5
  92. package/src/shared/utils/Result.ts +0 -20
  93. package/src/shared/utils/dateConverter.ts +6 -46
  94. package/src/utils/appUtils.ts +0 -16
  95. package/src/utils/creditMapper.ts +0 -7
  96. package/src/utils/dateUtils.compare.ts +0 -24
  97. package/src/utils/dateUtils.core.ts +0 -39
  98. package/src/utils/dateUtils.format.ts +0 -41
  99. package/src/utils/dateUtils.math.ts +0 -41
  100. package/src/utils/dateUtils.ts +0 -5
  101. package/src/utils/packagePeriodUtils.ts +0 -20
  102. package/src/utils/packageTypeDetector.ts +1 -21
  103. package/src/utils/premiumStatusUtils.ts +1 -14
  104. package/src/utils/priceUtils.ts +0 -35
  105. package/src/utils/tierUtils.ts +1 -8
  106. package/src/utils/types.ts +1 -25
  107. package/src/utils/validation.ts +1 -7
  108. package/src/domains/README.md +0 -52
  109. package/src/domains/config/domain/README.md +0 -37
  110. package/src/domains/config/domain/entities/README.md +0 -41
  111. package/src/domains/credits/application/credit-strategies/SyncCreditStrategy.ts +0 -24
  112. package/src/domains/paywall/README.md +0 -101
  113. package/src/domains/paywall/entities/README.md +0 -40
  114. package/src/domains/paywall/hooks/README.md +0 -41
  115. package/src/domains/subscription/application/syncConstants.ts +0 -1
  116. package/src/domains/subscription/infrastructure/README.md +0 -41
  117. package/src/domains/subscription/infrastructure/config/README.md +0 -49
  118. package/src/domains/subscription/infrastructure/handlers/README.md +0 -41
  119. package/src/domains/subscription/infrastructure/hooks/README.md +0 -50
  120. package/src/domains/subscription/infrastructure/managers/README.md +0 -41
  121. package/src/domains/subscription/infrastructure/services/README.md +0 -42
  122. package/src/domains/subscription/infrastructure/utils/README.md +0 -41
  123. package/src/domains/subscription/presentation/components/README.md +0 -155
  124. package/src/domains/subscription/presentation/components/details/CreditRow.md +0 -92
  125. package/src/domains/subscription/presentation/components/details/DetailRow.md +0 -91
  126. package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.md +0 -93
  127. package/src/domains/subscription/presentation/components/details/PremiumStatusBadge.md +0 -91
  128. package/src/domains/subscription/presentation/components/details/README.md +0 -99
  129. package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackModal.md +0 -90
  130. package/src/domains/subscription/presentation/components/feedback/README.md +0 -99
  131. package/src/domains/subscription/presentation/components/paywall/PaywallModal.md +0 -94
  132. package/src/domains/subscription/presentation/components/paywall/README.md +0 -54
  133. package/src/domains/subscription/presentation/components/sections/README.md +0 -99
  134. package/src/domains/subscription/presentation/components/sections/SubscriptionSection.md +0 -94
  135. package/src/domains/subscription/presentation/utils/README.md +0 -31
  136. package/src/domains/wallet/README.md +0 -51
  137. package/src/domains/wallet/domain/README.md +0 -41
  138. package/src/domains/wallet/infrastructure/README.md +0 -41
  139. package/src/domains/wallet/presentation/components/README.md +0 -41
  140. package/src/domains/wallet/presentation/hooks/README.md +0 -41
  141. 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
- ) => syncService.handlePurchase(u, p, c, s as any, pkgType),
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
- ) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt as any),
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
- periodType,
27
- packageType: null,
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
- PURCHASE_SOURCE.SETTINGS,
42
- revenueCatData,
43
- PURCHASE_TYPE.INITIAL
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
- PERIOD_TYPE,
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; // Raw value from RevenueCat SDK (NORMAL, INTRO, TRIAL)
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; // Lifetime
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; // Raw value from RevenueCat SDK (NORMAL, INTRO, TRIAL)
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
  }