@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.37.38",
3
+ "version": "2.37.40",
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",
@@ -1,20 +1,14 @@
1
1
  import type { CreditsConfig } from "../core/Credits";
2
2
  import { detectPackageType } from "../../../utils/packageTypeDetector";
3
3
  import { getCreditAllocation } from "../../../utils/creditMapper";
4
- import { NO_SUBSCRIPTION_PRODUCT_ID } from "../../subscription/application/syncConstants";
5
4
 
6
5
  export function calculateCreditLimit(productId: string | undefined, config: CreditsConfig): number {
7
6
  if (!productId) {
8
7
  throw new Error("[CreditLimitCalculator] Cannot calculate credit limit without productId");
9
8
  }
10
9
 
11
- // Free tier users (no subscription) get 0 credits - strict paywall
12
- if (productId === NO_SUBSCRIPTION_PRODUCT_ID) {
13
- return 0;
14
- }
15
-
16
10
  const explicitAmount = config.creditPackageAmounts?.[productId];
17
- if (explicitAmount) return explicitAmount;
11
+ if (explicitAmount !== undefined && explicitAmount !== null) return explicitAmount;
18
12
 
19
13
  const packageType = detectPackageType(productId);
20
14
  const dynamicLimit = getCreditAllocation(packageType, config.packageAllocations);
@@ -25,5 +19,3 @@ export function calculateCreditLimit(productId: string | undefined, config: Cred
25
19
 
26
20
  return dynamicLimit;
27
21
  }
28
-
29
-
@@ -4,7 +4,7 @@ import { getAppVersion, validatePlatform } from "../../../utils/appUtils";
4
4
  import type { InitializeCreditsMetadata, InitializationResult } from "../../subscription/application/SubscriptionInitializerTypes";
5
5
  import { runTransaction, type Transaction, type DocumentReference, type Firestore } from "@umituz/react-native-firebase";
6
6
  import { getCreditDocumentOrDefault } from "./creditDocumentHelpers";
7
- import { calculateNewCredits, buildCreditsData, shouldSkipStatusSyncWrite } from "./creditOperationUtils";
7
+ import { calculateNewCredits, buildCreditsData } from "./creditOperationUtils";
8
8
  import { calculateCreditLimit } from "./CreditLimitCalculator";
9
9
  import { generatePurchaseMetadata } from "./PurchaseMetadataGenerator";
10
10
  import { PURCHASE_ID_PREFIXES } from "../core/CreditsConstants";
@@ -17,23 +17,16 @@ export async function initializeCreditsTransaction(
17
17
  metadata: InitializeCreditsMetadata
18
18
  ): Promise<InitializationResult> {
19
19
 
20
+ if (!purchaseId.startsWith(PURCHASE_ID_PREFIXES.PURCHASE) && !purchaseId.startsWith(PURCHASE_ID_PREFIXES.RENEWAL)) {
21
+ throw new Error(`[CreditsInitializer] Only purchase and renewal operations can allocate credits. Received: ${purchaseId}`);
22
+ }
23
+
20
24
  const platform = validatePlatform();
21
25
  const appVersion = getAppVersion();
22
26
 
23
27
  return runTransaction(async (transaction: Transaction) => {
24
28
  const creditsDoc = await transaction.get(creditsRef);
25
29
 
26
- // Status sync must NEVER create new documents.
27
- // Credits documents are only created by purchase/renewal flows.
28
- const isStatusSync = purchaseId.startsWith(PURCHASE_ID_PREFIXES.STATUS_SYNC);
29
- if (isStatusSync && !creditsDoc.exists()) {
30
- return {
31
- credits: 0,
32
- alreadyProcessed: true,
33
- finalData: null
34
- };
35
- }
36
-
37
30
  const existingData = getCreditDocumentOrDefault(creditsDoc, platform);
38
31
 
39
32
  if (existingData.processedPurchases.includes(purchaseId)) {
@@ -71,14 +64,6 @@ export async function initializeCreditsTransaction(
71
64
  platform,
72
65
  });
73
66
 
74
- if (shouldSkipStatusSyncWrite(purchaseId, existingData, creditsData)) {
75
- return {
76
- credits: existingData.credits,
77
- alreadyProcessed: true,
78
- finalData: existingData
79
- };
80
- }
81
-
82
67
  transaction.set(creditsRef, creditsData, { merge: true });
83
68
 
84
69
  return {
@@ -3,10 +3,6 @@ import type { DeductCreditsResult } from "../core/Credits";
3
3
  import { CREDIT_ERROR_CODES } from "../core/CreditsConstants";
4
4
  import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
5
5
 
6
- /**
7
- * Deducts credits from a user's balance.
8
- * Encapsulates the domain rules and transaction logic for credit usage.
9
- */
10
6
  export async function deductCreditsOperation(
11
7
  _db: Firestore,
12
8
  creditsRef: DocumentReference,
@@ -24,6 +20,17 @@ export async function deductCreditsOperation(
24
20
  };
25
21
  }
26
22
 
23
+ if (cost <= 0 || !Number.isFinite(cost)) {
24
+ return {
25
+ success: false,
26
+ remainingCredits: null,
27
+ error: {
28
+ message: 'Cost must be a positive finite number',
29
+ code: 'INVALID_AMOUNT'
30
+ }
31
+ };
32
+ }
33
+
27
34
  try {
28
35
  const remaining = await runTransaction(async (tx: Transaction) => {
29
36
  const docSnap = await tx.get(creditsRef);
@@ -55,8 +62,8 @@ export async function deductCreditsOperation(
55
62
  };
56
63
  } catch (e: unknown) {
57
64
  const message = e instanceof Error ? e.message : String(e);
58
- const code = (message === CREDIT_ERROR_CODES.NO_CREDITS || message === CREDIT_ERROR_CODES.CREDITS_EXHAUSTED)
59
- ? message
65
+ const code = (message === CREDIT_ERROR_CODES.NO_CREDITS || message === CREDIT_ERROR_CODES.CREDITS_EXHAUSTED)
66
+ ? message
60
67
  : CREDIT_ERROR_CODES.DEDUCT_ERR;
61
68
 
62
69
  return {
@@ -3,10 +3,6 @@ import type { DeductCreditsResult } from "../core/Credits";
3
3
  import { CREDIT_ERROR_CODES } from "../core/CreditsConstants";
4
4
  import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
5
5
 
6
- /**
7
- * Refunds credits to a user's balance.
8
- * Used for optimistic billing rollbacks when generation fails due to transient errors.
9
- */
10
6
  export async function refundCreditsOperation(
11
7
  _db: Firestore,
12
8
  creditsRef: DocumentReference,
@@ -24,7 +20,7 @@ export async function refundCreditsOperation(
24
20
  };
25
21
  }
26
22
 
27
- if (amount <= 0) {
23
+ if (amount <= 0 || !Number.isFinite(amount)) {
28
24
  return {
29
25
  success: false,
30
26
  remainingCredits: null,
@@ -1,21 +1,13 @@
1
1
  import type { ICreditStrategy, CreditAllocationParams } from "./ICreditStrategy";
2
- import { SyncCreditStrategy } from "./SyncCreditStrategy";
3
2
  import { TrialCreditStrategy } from "./TrialCreditStrategy";
4
3
  import { StandardPurchaseCreditStrategy } from "./StandardPurchaseCreditStrategy";
5
4
 
6
- /**
7
- * Orchestrator to coordinate credit allocation logic using the Strategy Pattern.
8
- */
9
5
  class CreditAllocationOrchestrator {
10
6
  private strategies: ICreditStrategy[] = [
11
- new SyncCreditStrategy(),
12
7
  new TrialCreditStrategy(),
13
- new StandardPurchaseCreditStrategy(), // Fallback strategy
8
+ new StandardPurchaseCreditStrategy(),
14
9
  ];
15
10
 
16
- /**
17
- * Finds the first applicable strategy and executes its logic.
18
- */
19
11
  allocate(params: CreditAllocationParams): number {
20
12
  const strategy = this.strategies.find(s => s.canHandle(params));
21
13
 
@@ -1,9 +1,8 @@
1
1
  import type { SubscriptionStatusType } from "../../../subscription/core/SubscriptionStatus";
2
2
  import type { UserCreditsDocumentRead } from "../../core/UserCreditsDocument";
3
3
 
4
- export interface CreditStrategyParams {
4
+ export interface CreditAllocationParams {
5
5
  status: SubscriptionStatusType;
6
- isStatusSync: boolean;
7
6
  existingData: UserCreditsDocumentRead | null;
8
7
  creditLimit: number;
9
8
  isSubscriptionActive: boolean;
@@ -14,6 +13,3 @@ export interface ICreditStrategy {
14
13
  canHandle(params: CreditAllocationParams): boolean;
15
14
  execute(params: CreditAllocationParams): number;
16
15
  }
17
-
18
- // Renaming the input for clarity
19
- export type CreditAllocationParams = CreditStrategyParams;
@@ -2,13 +2,9 @@ import { ICreditStrategy, type CreditAllocationParams } from "./ICreditStrategy"
2
2
  import { SUBSCRIPTION_STATUS } from "../../../subscription/core/SubscriptionConstants";
3
3
  import { TRIAL_CONFIG } from "../../../trial/core/TrialTypes";
4
4
 
5
- /**
6
- * Strategy for Trial and Trial Canceled users.
7
- * Allocates credits based on trial configuration.
8
- */
9
5
  export class TrialCreditStrategy implements ICreditStrategy {
10
6
  canHandle(params: CreditAllocationParams): boolean {
11
- return params.status === SUBSCRIPTION_STATUS.TRIAL ||
7
+ return params.status === SUBSCRIPTION_STATUS.TRIAL ||
12
8
  params.status === SUBSCRIPTION_STATUS.TRIAL_CANCELED;
13
9
  }
14
10
 
@@ -1,14 +1,7 @@
1
- /**
2
- * Credit Document Helpers
3
- * Utilities for getting and creating credit documents
4
- */
5
-
6
1
  import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
7
2
  import { serverTimestamp, type DocumentSnapshot } from "@umituz/react-native-firebase";
8
3
  import { SUBSCRIPTION_STATUS, type Platform } from "../../subscription/core/SubscriptionConstants";
9
- /**
10
- * Get existing credit document or create default
11
- */
4
+
12
5
  export function getCreditDocumentOrDefault(
13
6
  creditsDoc: DocumentSnapshot,
14
7
  platform: Platform
@@ -17,7 +10,7 @@ export function getCreditDocumentOrDefault(
17
10
  return creditsDoc.data() as UserCreditsDocumentRead;
18
11
  }
19
12
 
20
- const now = serverTimestamp() as any; // FieldValue for Firestore write
13
+ const now = serverTimestamp() as any;
21
14
 
22
15
  const defaultDocument: UserCreditsDocumentRead = {
23
16
  credits: 0,
@@ -23,7 +23,6 @@ export function calculateNewCredits({ metadata, existingData, creditLimit, purch
23
23
 
24
24
  return creditAllocationOrchestrator.allocate({
25
25
  status,
26
- isStatusSync: purchaseId.startsWith(PURCHASE_ID_PREFIXES.STATUS_SYNC),
27
26
  existingData,
28
27
  creditLimit,
29
28
  isSubscriptionActive: isPremium && !isExpired,
@@ -49,7 +48,7 @@ export function buildCreditsData({
49
48
  periodType: metadata.periodType ?? undefined,
50
49
  });
51
50
 
52
- const isPurchaseOrRenewal = purchaseId.startsWith(PURCHASE_ID_PREFIXES.PURCHASE) ||
51
+ const isPurchaseOrRenewal = purchaseId.startsWith(PURCHASE_ID_PREFIXES.PURCHASE) ||
53
52
  purchaseId.startsWith(PURCHASE_ID_PREFIXES.RENEWAL);
54
53
 
55
54
  const expirationTimestamp = metadata.expirationDate ? toTimestamp(metadata.expirationDate) : null;
@@ -76,44 +75,3 @@ export function buildCreditsData({
76
75
  ...(metadata.ownershipType && { ownershipType: metadata.ownershipType }),
77
76
  };
78
77
  }
79
-
80
- /**
81
- * Compare two Firestore Timestamp-like values by their underlying time.
82
- * Handles Timestamp objects (with toMillis/seconds+nanoseconds), null, and undefined.
83
- */
84
- function timestampsEqual(a: unknown, b: unknown): boolean {
85
- if (a === b) return true;
86
- if (a == null || b == null) return a == b;
87
- if (typeof a === "object" && typeof b === "object" && a !== null && b !== null) {
88
- if ("toMillis" in a && "toMillis" in b &&
89
- typeof (a as { toMillis: unknown }).toMillis === "function" &&
90
- typeof (b as { toMillis: unknown }).toMillis === "function") {
91
- return (a as { toMillis: () => number }).toMillis() === (b as { toMillis: () => number }).toMillis();
92
- }
93
- if ("seconds" in a && "seconds" in b && "nanoseconds" in a && "nanoseconds" in b) {
94
- return (a as { seconds: number; nanoseconds: number }).seconds === (b as { seconds: number; nanoseconds: number }).seconds &&
95
- (a as { seconds: number; nanoseconds: number }).nanoseconds === (b as { seconds: number; nanoseconds: number }).nanoseconds;
96
- }
97
- }
98
- return false;
99
- }
100
-
101
- export function shouldSkipStatusSyncWrite(
102
- purchaseId: string,
103
- existingData: any,
104
- newCreditsData: Record<string, any>
105
- ): boolean {
106
- if (!purchaseId.startsWith(PURCHASE_ID_PREFIXES.STATUS_SYNC)) return false;
107
-
108
- if (!existingData || !newCreditsData) return false;
109
-
110
- return existingData.isPremium === newCreditsData.isPremium &&
111
- existingData.status === newCreditsData.status &&
112
- existingData.credits === newCreditsData.credits &&
113
- existingData.creditLimit === newCreditsData.creditLimit &&
114
- existingData.productId === newCreditsData.productId &&
115
- existingData.willRenew === newCreditsData.willRenew &&
116
- timestampsEqual(existingData.expirationDate, newCreditsData.expirationDate) &&
117
- timestampsEqual(existingData.canceledAt, newCreditsData.canceledAt) &&
118
- timestampsEqual(existingData.billingIssueDetectedAt, newCreditsData.billingIssueDetectedAt);
119
- }
@@ -1,12 +1,4 @@
1
- /**
2
- * Credits Domain Entities
3
- *
4
- * Generic credit system types for subscription-based apps.
5
- * Designed to be used across hundreds of apps with configurable limits.
6
- */
7
-
8
1
  import type { SubscriptionPackageType } from "../../../utils/packageTypeDetector";
9
- // Types imported from SubscriptionConstants are used directly in UserCredits interface
10
2
  import type {
11
3
  SubscriptionStatusType,
12
4
  PackageType,
@@ -17,37 +9,25 @@ import type {
17
9
 
18
10
  export type CreditType = "text" | "image";
19
11
 
20
- /** Single Source of Truth for user subscription + credits data */
21
12
  export interface UserCredits {
22
- // Core subscription
23
13
  isPremium: boolean;
24
14
  status: SubscriptionStatusType;
25
-
26
- // Dates
27
15
  purchasedAt: Date | null;
28
16
  expirationDate: Date | null;
29
17
  lastUpdatedAt: Date | null;
30
18
  lastPurchaseAt: Date | null;
31
-
32
- // RevenueCat subscription details
33
19
  willRenew: boolean | null;
34
20
  productId: string | null;
35
21
  packageType: PackageType | null;
36
22
  originalTransactionId: string | null;
37
-
38
- // Trial fields - periodType comes from RevenueCat SDK
39
23
  periodType: string | null;
40
24
  isTrialing: boolean | null;
41
25
  trialStartDate: Date | null;
42
26
  trialEndDate: Date | null;
43
27
  trialCredits: number | null;
44
28
  convertedFromTrial: boolean | null;
45
-
46
- // Credits
47
29
  credits: number;
48
30
  creditLimit: number;
49
-
50
- // Metadata
51
31
  purchaseSource: PurchaseSource | null;
52
32
  purchaseType: PurchaseType | null;
53
33
  platform: Platform;
@@ -66,11 +46,8 @@ export type PackageAllocationMap = Partial<Record<
66
46
  export interface CreditsConfig {
67
47
  collectionName: string;
68
48
  creditLimit: number;
69
- /** When true, stores credits at users/{userId}/credits instead of {collectionName}/{userId} */
70
49
  useUserSubcollection: boolean;
71
- /** Credit amounts per product ID for consumable credit packages */
72
50
  creditPackageAmounts: Record<string, number>;
73
- /** Credit allocations for different subscription types (weekly, monthly, yearly) */
74
51
  packageAllocations: PackageAllocationMap;
75
52
  }
76
53
 
@@ -1,6 +1,3 @@
1
- /**
2
- * Credit Error Codes
3
- */
4
1
  export const CREDIT_ERROR_CODES = {
5
2
  NO_CREDITS: 'NO_CREDITS',
6
3
  CREDITS_EXHAUSTED: 'CREDITS_EXHAUSTED',
@@ -8,17 +5,9 @@ export const CREDIT_ERROR_CODES = {
8
5
  DB_ERROR: 'ERR',
9
6
  } as const;
10
7
 
11
- /**
12
- * Purchase ID Prefixes
13
- */
14
8
  export const PURCHASE_ID_PREFIXES = {
15
- STATUS_SYNC: 'status_sync_',
16
9
  PURCHASE: 'purchase_',
17
10
  RENEWAL: 'renewal_',
18
11
  } as const;
19
12
 
20
- /**
21
- * Processed Purchases Array Window Size
22
- * Maintains last N purchases to prevent reprocessing
23
- */
24
13
  export const PROCESSED_PURCHASES_WINDOW = 50;
@@ -3,9 +3,6 @@ import { resolveSubscriptionStatus, type SubscriptionStatusType } from "../../su
3
3
  import type { UserCreditsDocumentRead } from "./UserCreditsDocument";
4
4
  import { toSafeDate } from "../../../utils/dateUtils";
5
5
 
6
- /**
7
- * Validate subscription status against expirationDate and periodType
8
- */
9
6
  function validateSubscription(
10
7
  doc: UserCreditsDocumentRead,
11
8
  expirationDate: Date | null,
@@ -28,9 +25,6 @@ function validateSubscription(
28
25
  };
29
26
  }
30
27
 
31
- /**
32
- * Maps Firestore document to domain entity with expiration validation
33
- */
34
28
  export function mapCreditsDocumentToEntity(doc: UserCreditsDocumentRead): UserCredits {
35
29
  const expirationDate = toSafeDate(doc.expirationDate);
36
30
  const periodType = doc.periodType;
@@ -30,41 +30,29 @@ export interface PurchaseMetadata {
30
30
  timestamp: FirestoreTimestamp;
31
31
  }
32
32
 
33
- /** Single Source of Truth for user subscription data */
34
33
  export interface UserCreditsDocumentRead {
35
- // Core subscription status
36
34
  isPremium: boolean;
37
35
  status: SubscriptionStatusType;
38
-
39
- // Dates (all from RevenueCat)
40
36
  purchasedAt: FirestoreTimestamp;
41
37
  expirationDate: FirestoreTimestamp | null;
42
38
  lastUpdatedAt: FirestoreTimestamp;
43
39
  lastPurchaseAt: FirestoreTimestamp | null;
44
40
  canceledAt: FirestoreTimestamp | null;
45
41
  billingIssueDetectedAt: FirestoreTimestamp | null;
46
-
47
- // RevenueCat subscription details
48
42
  willRenew: boolean | null;
49
43
  productId: string | null;
50
44
  packageType: PackageType | null;
51
45
  originalTransactionId: string | null;
52
46
  store: Store | null;
53
47
  ownershipType: OwnershipType | null;
54
-
55
- // Trial fields
56
48
  periodType: string | null;
57
49
  isTrialing: boolean | null;
58
50
  trialStartDate: FirestoreTimestamp | null;
59
51
  trialEndDate: FirestoreTimestamp | null;
60
52
  trialCredits: number | null;
61
53
  convertedFromTrial: boolean | null;
62
-
63
- // Credits
64
54
  credits: number;
65
55
  creditLimit: number;
66
-
67
- // Metadata
68
56
  purchaseSource: PurchaseSource | null;
69
57
  purchaseType: PurchaseType | null;
70
58
  platform: Platform;
@@ -8,7 +8,7 @@ import { refundCreditsOperation } from "../application/RefundCreditsCommand";
8
8
  import { PURCHASE_TYPE, type PurchaseType } from "../../subscription/core/SubscriptionConstants";
9
9
  import { requireFirestore, buildDocRef, type CollectionConfig } from "../../../shared/infrastructure/firestore";
10
10
  import { fetchCredits, checkHasCredits } from "./operations/CreditsFetcher";
11
- import { syncExpiredStatus } from "./operations/CreditsWriter";
11
+ import { syncExpiredStatus, syncPremiumMetadata, type PremiumMetadata } from "./operations/CreditsWriter";
12
12
  import { initializeCreditsWithRetry } from "./operations/CreditsInitializer";
13
13
 
14
14
  export class CreditsRepository extends BaseRepository {
@@ -74,6 +74,11 @@ export class CreditsRepository extends BaseRepository {
74
74
  const db = requireFirestore();
75
75
  await syncExpiredStatus(this.getRef(db, userId));
76
76
  }
77
+
78
+ async syncPremiumMetadata(userId: string, metadata: PremiumMetadata): Promise<void> {
79
+ const db = requireFirestore();
80
+ await syncPremiumMetadata(this.getRef(db, userId), metadata);
81
+ }
77
82
  }
78
83
 
79
84
  export function createCreditsRepository(config: CreditsConfig): CreditsRepository {
@@ -1,9 +1,3 @@
1
- /**
2
- * Credits Repository Manager
3
- * Module-level singleton for credits repository configuration
4
- * Provides a clean, testable approach for repository access
5
- */
6
-
7
1
  import type { CreditsConfig } from "../core/Credits";
8
2
  import type { CreditsRepository } from "./CreditsRepository";
9
3
  import { createCreditsRepository } from "./CreditsRepository";
@@ -11,26 +5,15 @@ import { createCreditsRepository } from "./CreditsRepository";
11
5
  let globalRepository: CreditsRepository | null = null;
12
6
  let globalConfig: CreditsConfig | null = null;
13
7
 
14
- /**
15
- * Configure credits repository for the application
16
- * Must be called once during app initialization
17
- */
18
8
  export function configureCreditsRepository(config: CreditsConfig): void {
19
9
  globalConfig = config;
20
10
  globalRepository = createCreditsRepository(globalConfig);
21
11
  }
22
12
 
23
- /**
24
- * Check if credits repository is configured
25
- */
26
13
  export function isCreditsRepositoryConfigured(): boolean {
27
14
  return globalRepository !== null;
28
15
  }
29
16
 
30
- /**
31
- * Get the configured credits repository
32
- * Throws if repository not configured
33
- */
34
17
  export function getCreditsRepository(): CreditsRepository {
35
18
  if (!globalRepository) {
36
19
  throw new Error(
@@ -40,9 +23,6 @@ export function getCreditsRepository(): CreditsRepository {
40
23
  return globalRepository;
41
24
  }
42
25
 
43
- /**
44
- * Get the current credits configuration
45
- */
46
26
  export function getCreditsConfig(): CreditsConfig {
47
27
  if (!globalConfig) {
48
28
  throw new Error(
@@ -51,4 +31,3 @@ export function getCreditsConfig(): CreditsConfig {
51
31
  }
52
32
  return globalConfig;
53
33
  }
54
-
@@ -1,9 +1,15 @@
1
- import { setDoc } from "firebase/firestore";
1
+ import { getDoc, setDoc } from "firebase/firestore";
2
2
  import type { DocumentReference } from "@umituz/react-native-firebase";
3
3
  import { serverTimestamp } from "@umituz/react-native-firebase";
4
4
  import { SUBSCRIPTION_STATUS } from "../../../subscription/core/SubscriptionConstants";
5
+ import { resolveSubscriptionStatus } from "../../../subscription/core/SubscriptionStatus";
6
+ import { toTimestamp } from "../../../../shared/utils/dateConverter";
7
+ import { isPast } from "../../../../utils/dateUtils";
5
8
 
6
9
  export async function syncExpiredStatus(ref: DocumentReference): Promise<void> {
10
+ const doc = await getDoc(ref);
11
+ if (!doc.exists()) return;
12
+
7
13
  await setDoc(ref, {
8
14
  isPremium: false,
9
15
  status: SUBSCRIPTION_STATUS.EXPIRED,
@@ -11,3 +17,48 @@ export async function syncExpiredStatus(ref: DocumentReference): Promise<void> {
11
17
  lastUpdatedAt: serverTimestamp(),
12
18
  }, { merge: true });
13
19
  }
20
+
21
+ export interface PremiumMetadata {
22
+ isPremium: boolean;
23
+ willRenew: boolean;
24
+ expirationDate: string | null;
25
+ productId: string;
26
+ periodType: string | null;
27
+ unsubscribeDetectedAt: string | null;
28
+ billingIssueDetectedAt: string | null;
29
+ store: string | null;
30
+ ownershipType: string | null;
31
+ }
32
+
33
+ export async function syncPremiumMetadata(
34
+ ref: DocumentReference,
35
+ metadata: PremiumMetadata
36
+ ): Promise<void> {
37
+ const doc = await getDoc(ref);
38
+ if (!doc.exists()) return;
39
+
40
+ const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
41
+ const status = resolveSubscriptionStatus({
42
+ isPremium: metadata.isPremium,
43
+ willRenew: metadata.willRenew,
44
+ isExpired,
45
+ periodType: metadata.periodType ?? undefined,
46
+ });
47
+
48
+ const expirationTimestamp = metadata.expirationDate ? toTimestamp(metadata.expirationDate) : null;
49
+ const canceledAtTimestamp = metadata.unsubscribeDetectedAt ? toTimestamp(metadata.unsubscribeDetectedAt) : null;
50
+ const billingIssueTimestamp = metadata.billingIssueDetectedAt ? toTimestamp(metadata.billingIssueDetectedAt) : null;
51
+
52
+ await setDoc(ref, {
53
+ isPremium: metadata.isPremium,
54
+ status,
55
+ willRenew: metadata.willRenew,
56
+ productId: metadata.productId,
57
+ lastUpdatedAt: serverTimestamp(),
58
+ ...(expirationTimestamp && { expirationDate: expirationTimestamp }),
59
+ ...(canceledAtTimestamp && { canceledAt: canceledAtTimestamp }),
60
+ ...(billingIssueTimestamp && { billingIssueDetectedAt: billingIssueTimestamp }),
61
+ ...(metadata.store && { store: metadata.store }),
62
+ ...(metadata.ownershipType && { ownershipType: metadata.ownershipType }),
63
+ }, { merge: true });
64
+ }
@@ -35,9 +35,9 @@ export const useDeductCredit = ({
35
35
  console.error('[useDeductCredit] Unexpected error during credit deduction', {
36
36
  cost,
37
37
  userId,
38
- error
38
+ error: error instanceof Error ? error.message : String(error)
39
39
  });
40
- throw error;
40
+ return false;
41
41
  }
42
42
  }, [onCreditsExhausted, userId]);
43
43
 
@@ -1,6 +1,6 @@
1
1
  import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
2
2
  import { useCallback, useMemo, useEffect } from "react";
3
- import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
3
+ import { useAuthStore, selectUserId, selectIsAnonymous } from "@umituz/react-native-auth";
4
4
  import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
5
5
  import { NO_CACHE_QUERY_CONFIG } from "../../../shared/infrastructure/react-query/queryConfig";
6
6
  import { usePreviousUserCleanup } from "../../../shared/infrastructure/react-query/hooks/usePreviousUserCleanup";
@@ -10,7 +10,7 @@ import {
10
10
  isCreditsRepositoryConfigured,
11
11
  } from "../infrastructure/CreditsRepositoryManager";
12
12
  import { calculateSafePercentage, canAffordAmount } from "../utils/creditValidation";
13
- import { isAuthenticated } from "../../subscription/utils/authGuards";
13
+ import { isRegisteredUser } from "../../subscription/utils/authGuards";
14
14
  import { creditsQueryKeys } from "./creditsQueryKeys";
15
15
  import type { UseCreditsResult, CreditsLoadStatus } from "./useCredits.types";
16
16
 
@@ -26,15 +26,17 @@ const deriveLoadStatus = (
26
26
 
27
27
  export const useCredits = (): UseCreditsResult => {
28
28
  const userId = useAuthStore(selectUserId);
29
+ const isAnonymous = useAuthStore(selectIsAnonymous);
29
30
  const isConfigured = isCreditsRepositoryConfigured();
30
31
 
31
32
  const config = isConfigured ? getCreditsConfig() : null;
32
- const queryEnabled = isAuthenticated(userId) && isConfigured;
33
+ const isUserRegistered = isRegisteredUser(userId, isAnonymous);
34
+ const queryEnabled = isUserRegistered && isConfigured;
33
35
 
34
36
  const { data, status, error, refetch } = useQuery({
35
37
  queryKey: creditsQueryKeys.user(userId),
36
38
  queryFn: async () => {
37
- if (!isAuthenticated(userId) || !isConfigured) return null;
39
+ if (!isUserRegistered || !isConfigured) return null;
38
40
 
39
41
  const repository = getCreditsRepository();
40
42
  const result = await repository.getCredits(userId);
@@ -51,11 +53,10 @@ export const useCredits = (): UseCreditsResult => {
51
53
 
52
54
  const queryClient = useQueryClient();
53
55
 
54
- // Clean up previous user's cache on logout/user switch
55
56
  usePreviousUserCleanup(userId, queryClient, creditsQueryKeys.user);
56
57
 
57
58
  useEffect(() => {
58
- if (!isAuthenticated(userId)) return undefined;
59
+ if (!isUserRegistered) return undefined;
59
60
 
60
61
  const unsubscribe = subscriptionEventBus.on(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, (updatedUserId) => {
61
62
  if (updatedUserId === userId) {
@@ -64,13 +65,13 @@ export const useCredits = (): UseCreditsResult => {
64
65
  });
65
66
 
66
67
  return unsubscribe;
67
- }, [userId, queryClient]);
68
+ }, [userId, isUserRegistered, queryClient]);
68
69
 
69
70
  const credits = data ?? null;
70
71
 
71
72
  const derivedValues = useMemo(() => {
72
73
  const has = (credits?.credits ?? 0) > 0;
73
- const limit = config?.creditLimit ?? 0;
74
+ const limit = credits?.creditLimit ?? config?.creditLimit ?? 0;
74
75
  const percent = calculateSafePercentage(credits?.credits, limit);
75
76
  return { hasCredits: has, creditsPercent: percent };
76
77
  }, [credits, config?.creditLimit]);
@@ -95,4 +96,4 @@ export const useCredits = (): UseCreditsResult => {
95
96
  refetch,
96
97
  canAfford,
97
98
  };
98
- };
99
+ };