@umituz/react-native-subscription 2.27.65 → 2.27.67

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 (121) hide show
  1. package/package.json +3 -1
  2. package/src/domains/credits/application/CreditLimitCalculator.ts +17 -0
  3. package/src/domains/credits/application/CreditsInitializer.ts +85 -0
  4. package/src/domains/credits/application/DeductCreditsCommand.ts +52 -0
  5. package/src/domains/credits/application/PurchaseMetadataGenerator.ts +59 -0
  6. package/src/domains/credits/application/credit-strategies/CreditAllocationContext.ts +35 -0
  7. package/src/domains/credits/application/credit-strategies/ICreditStrategy.ts +18 -0
  8. package/src/domains/credits/application/credit-strategies/StandardPurchaseCreditStrategy.ts +16 -0
  9. package/src/domains/credits/application/credit-strategies/SyncCreditStrategy.ts +15 -0
  10. package/src/domains/credits/application/credit-strategies/TrialCreditStrategy.ts +18 -0
  11. package/src/{infrastructure/mappers → domains/credits/core}/CreditsMapper.ts +4 -4
  12. package/src/domains/credits/infrastructure/CreditsRepository.ts +102 -0
  13. package/src/{presentation/hooks → domains/credits/presentation}/useCredits.ts +21 -4
  14. package/src/domains/subscription/application/SubscriptionAuthListener.ts +26 -0
  15. package/src/domains/subscription/application/SubscriptionInitializer.ts +77 -0
  16. package/src/{infrastructure/services → domains/subscription/application}/SubscriptionInitializerTypes.ts +21 -1
  17. package/src/domains/subscription/application/SubscriptionSyncService.ts +71 -0
  18. package/src/domains/subscription/application/SubscriptionSyncUtils.ts +16 -0
  19. package/src/{revenuecat/domain/value-objects → domains/subscription/core}/RevenueCatConfig.ts +1 -1
  20. package/src/{domain/types → domains/subscription/core}/RevenueCatData.ts +1 -1
  21. package/src/{domain/entities → domains/subscription/core}/SubscriptionStatus.ts +13 -21
  22. package/src/domains/subscription/core/SubscriptionStatusHandlers.ts +51 -0
  23. package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +67 -0
  24. package/src/domains/subscription/infrastructure/handlers/PurchaseStatusResolver.ts +27 -0
  25. package/src/domains/subscription/infrastructure/managers/SubscriptionInternalState.ts +12 -0
  26. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +110 -0
  27. package/src/{presentation/hooks → domains/subscription/presentation}/usePremium.ts +7 -4
  28. package/src/domains/trial/application/TrialEligibilityService.ts +25 -0
  29. package/src/domains/trial/application/TrialService.ts +68 -0
  30. package/src/{infrastructure/services → domains/trial/core}/TrialTypes.ts +1 -1
  31. package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +30 -0
  32. package/src/presentation/components/details/PremiumStatusBadge.tsx +2 -2
  33. package/src/presentation/hooks/index.ts +11 -11
  34. package/src/shared/infrastructure/SubscriptionEventBus.ts +51 -0
  35. package/src/utils/packageTypeDetector.ts +13 -18
  36. package/src/application/README.md +0 -50
  37. package/src/domain/entities/README.md +0 -50
  38. package/src/domain/entities/SubscriptionStatus.test.ts +0 -105
  39. package/src/domain/errors/README.md +0 -53
  40. package/src/domain/value-objects/README.md +0 -50
  41. package/src/infrastructure/README.md +0 -55
  42. package/src/infrastructure/mappers/README.md +0 -21
  43. package/src/infrastructure/models/README.md +0 -26
  44. package/src/infrastructure/repositories/CreditsRepository.ts +0 -132
  45. package/src/infrastructure/repositories/README.md +0 -99
  46. package/src/infrastructure/services/CreditsInitializer.ts +0 -170
  47. package/src/infrastructure/services/README.md +0 -99
  48. package/src/infrastructure/services/SubscriptionInitializer.ts +0 -176
  49. package/src/infrastructure/services/SubscriptionService.ts +0 -133
  50. package/src/infrastructure/services/TrialService.ts +0 -197
  51. package/src/infrastructure/services/app-service-helpers.ts +0 -111
  52. package/src/revenuecat/README.md +0 -104
  53. package/src/revenuecat/application/README.md +0 -43
  54. package/src/revenuecat/application/ports/IRevenueCatService.ts +0 -76
  55. package/src/revenuecat/application/ports/README.md +0 -41
  56. package/src/revenuecat/domain/README.md +0 -48
  57. package/src/revenuecat/domain/constants/README.md +0 -41
  58. package/src/revenuecat/domain/entities/README.md +0 -42
  59. package/src/revenuecat/domain/errors/README.md +0 -53
  60. package/src/revenuecat/domain/types/README.md +0 -41
  61. package/src/revenuecat/domain/value-objects/README.md +0 -41
  62. package/src/revenuecat/index.ts +0 -13
  63. package/src/revenuecat/infrastructure/handlers/PackageHandler.ts +0 -161
  64. package/src/revenuecat/infrastructure/managers/SubscriptionManager.ts +0 -165
  65. package/src/revenuecat/presentation/README.md +0 -42
  66. /package/src/{domain/entities → domains/credits/core}/Credits.ts +0 -0
  67. /package/src/{infrastructure/models → domains/credits/core}/UserCreditsDocument.ts +0 -0
  68. /package/src/{infrastructure/repositories → domains/credits/infrastructure}/CreditsRepositoryProvider.ts +0 -0
  69. /package/src/{presentation/hooks → domains/credits/presentation}/useDeductCredit.ts +0 -0
  70. /package/src/{revenuecat/domain/constants → domains/subscription/core}/RevenueCatConstants.ts +0 -0
  71. /package/src/{revenuecat/domain/errors → domains/subscription/core}/RevenueCatError.ts +0 -0
  72. /package/src/{revenuecat/domain/types → domains/subscription/core}/RevenueCatTypes.ts +0 -0
  73. /package/src/{domain/entities → domains/subscription/core}/SubscriptionConstants.ts +0 -0
  74. /package/src/{revenuecat → domains/subscription}/infrastructure/README.md +0 -0
  75. /package/src/{revenuecat → domains/subscription}/infrastructure/config/README.md +0 -0
  76. /package/src/{revenuecat → domains/subscription}/infrastructure/handlers/README.md +0 -0
  77. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/README.md +0 -0
  78. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/subscriptionQueryKeys.ts +0 -0
  79. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useCustomerInfo.ts +0 -0
  80. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useInitializeSubscription.ts +0 -0
  81. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/usePaywallFlow.ts +0 -0
  82. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/usePurchasePackage.ts +0 -0
  83. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRestorePurchase.ts +0 -0
  84. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRevenueCat.ts +0 -0
  85. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRevenueCatTrialEligibility.ts +0 -0
  86. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useSubscriptionPackages.ts +0 -0
  87. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useSubscriptionQueries.ts +0 -0
  88. /package/src/{revenuecat → domains/subscription}/infrastructure/managers/README.md +0 -0
  89. /package/src/{revenuecat → domains/subscription}/infrastructure/services/CustomerInfoListenerManager.ts +0 -0
  90. /package/src/{revenuecat → domains/subscription}/infrastructure/services/OfferingsFetcher.ts +0 -0
  91. /package/src/{revenuecat → domains/subscription}/infrastructure/services/PurchaseHandler.ts +0 -0
  92. /package/src/{revenuecat → domains/subscription}/infrastructure/services/README.md +0 -0
  93. /package/src/{revenuecat → domains/subscription}/infrastructure/services/RestoreHandler.ts +0 -0
  94. /package/src/{revenuecat → domains/subscription}/infrastructure/services/RevenueCatInitializer.ts +0 -0
  95. /package/src/{revenuecat → domains/subscription}/infrastructure/services/RevenueCatService.ts +0 -0
  96. /package/src/{revenuecat → domains/subscription}/infrastructure/services/ServiceStateManager.ts +0 -0
  97. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/ApiKeyResolver.ts +0 -0
  98. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/InitializationCache.ts +0 -0
  99. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/PremiumStatusSyncer.ts +0 -0
  100. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/README.md +0 -0
  101. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/RenewalDetector.ts +0 -0
  102. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/UserIdProvider.ts +0 -0
  103. /package/src/{presentation/hooks → domains/subscription/presentation}/useAuthAwarePurchase.ts +0 -0
  104. /package/src/{presentation/hooks → domains/subscription/presentation}/useAuthSubscriptionSync.ts +0 -0
  105. /package/src/{presentation/hooks → domains/subscription/presentation}/useFeatureGate.ts +0 -0
  106. /package/src/{presentation/hooks → domains/subscription/presentation}/usePaywallVisibility.ts +0 -0
  107. /package/src/{presentation/hooks → domains/subscription/presentation}/usePremiumGate.ts +0 -0
  108. /package/src/{presentation/hooks → domains/subscription/presentation}/useSavedPurchaseAutoExecution.ts +0 -0
  109. /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionSettingsConfig.ts +0 -0
  110. /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionSettingsConfig.utils.ts +0 -0
  111. /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionStatus.ts +0 -0
  112. /package/src/{infrastructure/services → shared/application}/ActivationHandler.ts +0 -0
  113. /package/src/{infrastructure/services → shared/application}/FeedbackService.ts +0 -0
  114. /package/src/{application → shared/application}/ports/ISubscriptionRepository.ts +0 -0
  115. /package/src/{application → shared/application}/ports/ISubscriptionService.ts +0 -0
  116. /package/src/{application → shared/application}/ports/README.md +0 -0
  117. /package/src/{domain/errors → shared/utils}/InsufficientCreditsError.ts +0 -0
  118. /package/src/{infrastructure → shared}/utils/Logger.ts +0 -0
  119. /package/src/{domain/value-objects → shared/utils}/Result.ts +0 -0
  120. /package/src/{domain/value-objects → shared/utils}/SubscriptionConfig.ts +0 -0
  121. /package/src/{domain/errors → shared/utils}/SubscriptionError.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.27.65",
3
+ "version": "2.27.67",
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",
@@ -55,7 +55,9 @@
55
55
  "@tanstack/query-async-storage-persister": "^5.66.7",
56
56
  "@tanstack/react-query": "^5.0.0",
57
57
  "@tanstack/react-query-persist-client": "^5.66.7",
58
+ "@types/jest": "^30.0.0",
58
59
  "@types/react": "~19.1.10",
60
+ "@types/react-native": "^0.72.8",
59
61
  "@typescript-eslint/eslint-plugin": "^8.50.1",
60
62
  "@typescript-eslint/parser": "^8.50.1",
61
63
  "@umituz/react-native-auth": "^3.6.14",
@@ -0,0 +1,17 @@
1
+ import type { CreditsConfig } from "../../domain/entities/Credits";
2
+ import { detectPackageType } from "../../utils/packageTypeDetector";
3
+ import { getCreditAllocation } from "../../utils/creditMapper";
4
+
5
+ export class CreditLimitCalculator {
6
+ static calculate(productId: string | undefined, config: CreditsConfig): number {
7
+ if (!productId) return config.creditLimit;
8
+
9
+ const explicitAmount = config.creditPackageAmounts?.[productId];
10
+ if (explicitAmount) return explicitAmount;
11
+
12
+ const packageType = detectPackageType(productId);
13
+ const dynamicLimit = getCreditAllocation(packageType, config.packageAllocations);
14
+
15
+ return dynamicLimit ?? config.creditLimit;
16
+ }
17
+ }
@@ -0,0 +1,85 @@
1
+ import { Platform } from "react-native";
2
+ import Constants from "expo-constants";
3
+ import {
4
+ runTransaction,
5
+ serverTimestamp,
6
+ Timestamp,
7
+ type Firestore,
8
+ type Transaction,
9
+ type DocumentReference,
10
+ } from "firebase/firestore";
11
+ import type { CreditsConfig } from "../core/Credits";
12
+ import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
13
+ import { resolveSubscriptionStatus } from "../../subscription/core/SubscriptionStatus";
14
+ import { CreditLimitCalculator } from "./CreditLimitCalculator";
15
+ import { PurchaseMetadataGenerator } from "./PurchaseMetadataGenerator";
16
+ import { creditAllocationContext } from "./credit-strategies/CreditAllocationContext";
17
+ import type { InitializeCreditsMetadata, InitializationResult } from "../../subscription/application/SubscriptionInitializerTypes";
18
+
19
+ export async function initializeCreditsTransaction(
20
+ db: Firestore,
21
+ creditsRef: DocumentReference,
22
+ config: CreditsConfig,
23
+ purchaseId?: string,
24
+ metadata?: InitializeCreditsMetadata
25
+ ): Promise<InitializationResult> {
26
+ return runTransaction(db, async (transaction: Transaction) => {
27
+ const creditsDoc = await transaction.get(creditsRef);
28
+ const now = serverTimestamp();
29
+ const existingData = creditsDoc.exists() ? creditsDoc.data() as UserCreditsDocumentRead : null;
30
+
31
+ if (existingData && purchaseId && existingData.processedPurchases?.includes(purchaseId)) {
32
+ return { credits: existingData.credits, alreadyProcessed: true };
33
+ }
34
+
35
+ const creditLimit = CreditLimitCalculator.calculate(metadata?.productId, config);
36
+ const { purchaseType, purchaseHistory } = PurchaseMetadataGenerator.generate({
37
+ productId: metadata?.productId,
38
+ source: metadata?.source,
39
+ type: metadata?.type,
40
+ creditLimit,
41
+ platform: Platform.OS as "ios" | "android",
42
+ appVersion: Constants.expoConfig?.version,
43
+ }, existingData);
44
+
45
+ const isPremium = metadata?.isPremium ?? true;
46
+ const isExpired = metadata?.expirationDate ? new Date(metadata.expirationDate).getTime() < Date.now() : false;
47
+ const status = resolveSubscriptionStatus({
48
+ isPremium, willRenew: metadata?.willRenew, isExpired, periodType: metadata?.periodType
49
+ });
50
+
51
+ // Resolve credits using Strategy Pattern
52
+ const isStatusSync = purchaseId?.startsWith("status_sync_") ?? false;
53
+ const isSubscriptionActive = isPremium && !isExpired;
54
+
55
+ const newCredits = creditAllocationContext.allocate({
56
+ status,
57
+ isStatusSync,
58
+ existingData,
59
+ creditLimit,
60
+ isSubscriptionActive
61
+ });
62
+
63
+ const creditsData: Record<string, any> = {
64
+ isPremium, status, credits: newCredits, creditLimit,
65
+ lastUpdatedAt: now,
66
+ processedPurchases: (purchaseId ? [...(existingData?.processedPurchases || []), purchaseId].slice(-10) : existingData?.processedPurchases) || [],
67
+ purchaseHistory: purchaseHistory.length ? purchaseHistory : undefined
68
+ };
69
+
70
+ const isNewPurchaseOrRenewal = purchaseId?.startsWith("purchase_") || purchaseId?.startsWith("renewal_");
71
+ if (isNewPurchaseOrRenewal) creditsData.lastPurchaseAt = now;
72
+ if (metadata?.expirationDate) creditsData.expirationDate = Timestamp.fromDate(new Date(metadata.expirationDate));
73
+ if (metadata?.willRenew !== undefined) creditsData.willRenew = metadata.willRenew;
74
+ if (metadata?.originalTransactionId) creditsData.originalTransactionId = metadata.originalTransactionId;
75
+ if (metadata?.productId) {
76
+ creditsData.productId = metadata.productId;
77
+ creditsData.platform = Platform.OS;
78
+ }
79
+
80
+ transaction.set(creditsRef, creditsData, { merge: true });
81
+
82
+ const finalData = { ...(existingData || {}), ...creditsData } as UserCreditsDocumentRead;
83
+ return { credits: newCredits, finalData };
84
+ });
85
+ }
@@ -0,0 +1,52 @@
1
+ import { runTransaction, serverTimestamp, type Firestore, type Transaction, type DocumentReference } from "firebase/firestore";
2
+ import { getFirestore } from "@umituz/react-native-firebase";
3
+ import type { DeductCreditsResult } from "../../../domain/entities/Credits";
4
+ import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../SubscriptionEventBus";
5
+
6
+ export interface IDeductCreditsCommand {
7
+ execute(userId: string, cost: number): Promise<DeductCreditsResult>;
8
+ }
9
+
10
+ /**
11
+ * Command for deducting credits.
12
+ * Encapsulates the domain rules and transaction logic for credit usage.
13
+ */
14
+ export class DeductCreditsCommand implements IDeductCreditsCommand {
15
+ constructor(
16
+ private getCreditsRef: (db: Firestore, userId: string) => DocumentReference
17
+ ) {}
18
+
19
+ async execute(userId: string, cost: number = 1): Promise<DeductCreditsResult> {
20
+ const db = getFirestore();
21
+ if (!db) return { success: false, error: { message: "No DB", code: "ERR" } };
22
+
23
+ try {
24
+ const remaining = await runTransaction(db, async (tx: Transaction) => {
25
+ const ref = this.getCreditsRef(db, userId);
26
+ const docSnap = await tx.get(ref);
27
+
28
+ if (!docSnap.exists()) throw new Error("NO_CREDITS");
29
+
30
+ const current = docSnap.data().credits as number;
31
+ if (current < cost) throw new Error("CREDITS_EXHAUSTED");
32
+
33
+ const updated = current - cost;
34
+ tx.update(ref, {
35
+ credits: updated,
36
+ lastUpdatedAt: serverTimestamp()
37
+ });
38
+
39
+ return updated;
40
+ });
41
+
42
+ // Emit event via EventBus (Observer Pattern)
43
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
44
+
45
+ return { success: true, remainingCredits: remaining };
46
+ } catch (e: unknown) {
47
+ const message = e instanceof Error ? e.message : String(e);
48
+ const code = message === "NO_CREDITS" || message === "CREDITS_EXHAUSTED" ? message : "DEDUCT_ERR";
49
+ return { success: false, error: { message, code } };
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,59 @@
1
+ import { Timestamp } from "firebase/firestore";
2
+ import type {
3
+ PurchaseType,
4
+ PurchaseMetadata,
5
+ UserCreditsDocumentRead,
6
+ PurchaseSource
7
+ } from "../models/UserCreditsDocument";
8
+ import { detectPackageType } from "../../utils/packageTypeDetector";
9
+
10
+ export interface MetadataGeneratorConfig {
11
+ productId?: string;
12
+ source?: PurchaseSource;
13
+ type?: PurchaseType;
14
+ creditLimit: number;
15
+ platform: "ios" | "android";
16
+ appVersion?: string;
17
+ }
18
+
19
+ export class PurchaseMetadataGenerator {
20
+ static generate(
21
+ config: MetadataGeneratorConfig,
22
+ existingData: UserCreditsDocumentRead | null
23
+ ): { purchaseType: PurchaseType; purchaseHistory: PurchaseMetadata[] } {
24
+ const { productId, source, type, creditLimit, platform, appVersion } = config;
25
+
26
+ if (!productId || !source) {
27
+ return {
28
+ purchaseType: type ?? "initial",
29
+ purchaseHistory: existingData?.purchaseHistory || []
30
+ };
31
+ }
32
+
33
+ const packageType = detectPackageType(productId);
34
+ let purchaseType: PurchaseType = type ?? "initial";
35
+
36
+ if (existingData?.packageType && packageType !== "unknown") {
37
+ const oldLimit = existingData.creditLimit || 0;
38
+ if (creditLimit > oldLimit) purchaseType = "upgrade";
39
+ else if (creditLimit < oldLimit) purchaseType = "downgrade";
40
+ // This check is a bit fragile if purchaseId is not passed here,
41
+ // but we use the explicit 'type' if provided.
42
+ }
43
+
44
+ const newMetadata: PurchaseMetadata = {
45
+ productId,
46
+ packageType,
47
+ creditLimit,
48
+ source,
49
+ type: purchaseType,
50
+ platform,
51
+ appVersion,
52
+ timestamp: Timestamp.fromDate(new Date()) as any,
53
+ };
54
+
55
+ const purchaseHistory = [...(existingData?.purchaseHistory || []), newMetadata].slice(-10);
56
+
57
+ return { purchaseType, purchaseHistory };
58
+ }
59
+ }
@@ -0,0 +1,35 @@
1
+ import type { ICreditStrategy, CreditAllocationParams } from "./ICreditStrategy";
2
+ import { SyncCreditStrategy } from "./SyncCreditStrategy";
3
+ import { TrialCreditStrategy } from "./TrialCreditStrategy";
4
+ import { StandardPurchaseCreditStrategy } from "./StandardPurchaseCreditStrategy";
5
+
6
+ /**
7
+ * Strategy Context to coordinate credit allocation logic using the Strategy Pattern.
8
+ */
9
+ export class CreditAllocationContext {
10
+ private strategies: ICreditStrategy[] = [
11
+ new SyncCreditStrategy(),
12
+ new TrialCreditStrategy(),
13
+ new StandardPurchaseCreditStrategy(), // Fallback strategy
14
+ ];
15
+
16
+ /**
17
+ * Finds the first applicable strategy and executes its logic.
18
+ */
19
+ allocate(params: CreditAllocationParams): number {
20
+ const strategy = this.strategies.find(s => s.canHandle(params));
21
+
22
+ if (!strategy) {
23
+ // Should theoretically never happen due to StandardPurchaseCreditStrategy fallback
24
+ return params.creditLimit;
25
+ }
26
+
27
+ if (__DEV__) {
28
+ console.log(`[CreditAllocationContext] Using strategy: ${strategy.constructor.name}`);
29
+ }
30
+
31
+ return strategy.execute(params);
32
+ }
33
+ }
34
+
35
+ export const creditAllocationContext = new CreditAllocationContext();
@@ -0,0 +1,18 @@
1
+ import type { SubscriptionStatusType } from "../../domain/entities/SubscriptionStatus";
2
+ import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
3
+
4
+ export interface CreditStrategyParams {
5
+ status: SubscriptionStatusType;
6
+ isStatusSync: boolean;
7
+ existingData: UserCreditsDocumentRead | null;
8
+ creditLimit: number;
9
+ isSubscriptionActive: boolean;
10
+ }
11
+
12
+ export interface ICreditStrategy {
13
+ canHandle(params: CreditAllocationParams): boolean;
14
+ execute(params: CreditAllocationParams): number;
15
+ }
16
+
17
+ // Renaming the input for clarity
18
+ export type CreditAllocationParams = CreditStrategyParams;
@@ -0,0 +1,16 @@
1
+ import { ICreditStrategy, type CreditAllocationParams } from "./ICreditStrategy";
2
+
3
+ /**
4
+ * Default strategy for new purchases, renewals, or upgrades.
5
+ * Resets credits to the calculated credit limit.
6
+ */
7
+ export class StandardPurchaseCreditStrategy implements ICreditStrategy {
8
+ canHandle(_params: CreditAllocationParams): boolean {
9
+ // This is a catch-all strategy
10
+ return true;
11
+ }
12
+
13
+ execute(params: CreditAllocationParams): number {
14
+ return params.creditLimit;
15
+ }
16
+ }
@@ -0,0 +1,15 @@
1
+ import { ICreditStrategy, type CreditAllocationParams } from "./ICreditStrategy";
2
+
3
+ /**
4
+ * Strategy for existing premium users during a simple status synchronization.
5
+ * Preserves their current credits to avoid accidental resets.
6
+ */
7
+ export class SyncCreditStrategy implements ICreditStrategy {
8
+ canHandle(params: CreditAllocationParams): boolean {
9
+ return params.isStatusSync && params.existingData?.isPremium === true && params.isSubscriptionActive;
10
+ }
11
+
12
+ execute(params: CreditAllocationParams): number {
13
+ return params.existingData?.credits ?? params.creditLimit;
14
+ }
15
+ }
@@ -0,0 +1,18 @@
1
+ import { ICreditStrategy, type CreditAllocationParams } from "./ICreditStrategy";
2
+ import { SUBSCRIPTION_STATUS } from "../../../domain/entities/SubscriptionStatus";
3
+ import { TRIAL_CONFIG } from "../TrialService";
4
+
5
+ /**
6
+ * Strategy for Trial and Trial Canceled users.
7
+ * Allocates credits based on trial configuration.
8
+ */
9
+ export class TrialCreditStrategy implements ICreditStrategy {
10
+ canHandle(params: CreditAllocationParams): boolean {
11
+ return params.status === SUBSCRIPTION_STATUS.TRIAL ||
12
+ params.status === SUBSCRIPTION_STATUS.TRIAL_CANCELED;
13
+ }
14
+
15
+ execute(_params: CreditAllocationParams): number {
16
+ return TRIAL_CONFIG.CREDITS;
17
+ }
18
+ }
@@ -1,7 +1,7 @@
1
- import type { UserCredits } from "../../domain/entities/Credits";
2
- import { resolveSubscriptionStatus } from "../../domain/entities/SubscriptionStatus";
3
- import type { PeriodType, SubscriptionStatusType } from "../../domain/entities/SubscriptionConstants";
4
- import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
1
+ import type { UserCredits } from "./Credits";
2
+ import { resolveSubscriptionStatus } from "../../subscription/core/SubscriptionStatus";
3
+ import type { PeriodType, SubscriptionStatusType } from "../../subscription/core/SubscriptionConstants";
4
+ import type { UserCreditsDocumentRead } from "./UserCreditsDocument";
5
5
 
6
6
  /** Maps Firestore document to domain entity with expiration validation */
7
7
  export class CreditsMapper {
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Credits Repository
3
+ * Optimized to use Design Patterns: Command, Observer, and Strategy.
4
+ */
5
+
6
+ import { doc, getDoc, serverTimestamp, updateDoc, type Firestore } from "firebase/firestore";
7
+ import { BaseRepository, getFirestore } from "@umituz/react-native-firebase";
8
+ import type { CreditsConfig, CreditsResult, DeductCreditsResult } from "../core/Credits";
9
+ import type { UserCreditsDocumentRead, PurchaseSource } from "../core/UserCreditsDocument";
10
+ import { initializeCreditsTransaction } from "../application/CreditsInitializer";
11
+ import { CreditsMapper } from "../core/CreditsMapper";
12
+ import type { RevenueCatData } from "../../subscription/core/RevenueCatData";
13
+ import { DeductCreditsCommand } from "../application/DeductCreditsCommand";
14
+ import { CreditLimitCalculator } from "../application/CreditLimitCalculator";
15
+
16
+ export class CreditsRepository extends BaseRepository {
17
+ private deductCommand: DeductCreditsCommand;
18
+
19
+ constructor(private config: CreditsConfig) {
20
+ super();
21
+ this.deductCommand = new DeductCreditsCommand((db, uid) => this.getRef(db, uid));
22
+ }
23
+
24
+ private getRef(db: Firestore, userId: string) {
25
+ return this.config.useUserSubcollection
26
+ ? doc(db, "users", userId, "credits", "balance")
27
+ : doc(db, this.config.collectionName, userId);
28
+ }
29
+
30
+ async getCredits(userId: string): Promise<CreditsResult> {
31
+ const db = getFirestore();
32
+ if (!db) return { success: false, error: { message: "No DB", code: "DB_ERR" } };
33
+ try {
34
+ const snap = await getDoc(this.getRef(db, userId));
35
+ if (!snap.exists()) return { success: true, data: undefined };
36
+
37
+ const entity = CreditsMapper.toEntity(snap.data() as UserCreditsDocumentRead);
38
+ return { success: true, data: entity };
39
+ } catch (e: unknown) {
40
+ const message = e instanceof Error ? e.message : String(e);
41
+ return { success: false, error: { message, code: "FETCH_ERR" } };
42
+ }
43
+ }
44
+
45
+ async initializeCredits(
46
+ userId: string, purchaseId?: string, productId?: string,
47
+ source?: PurchaseSource, revenueCatData?: RevenueCatData
48
+ ): Promise<CreditsResult> {
49
+ const db = getFirestore();
50
+ if (!db) return { success: false, error: { message: "No DB", code: "INIT_ERR" } };
51
+ try {
52
+ // Use CreditLimitCalculator (Refactoring Logic)
53
+ const creditLimit = CreditLimitCalculator.calculate(productId, this.config);
54
+ const cfg = { ...this.config, creditLimit };
55
+
56
+ const result = await initializeCreditsTransaction(db, this.getRef(db, userId), cfg, purchaseId, {
57
+ productId, source,
58
+ expirationDate: revenueCatData?.expirationDate,
59
+ willRenew: revenueCatData?.willRenew,
60
+ originalTransactionId: revenueCatData?.originalTransactionId,
61
+ isPremium: revenueCatData?.isPremium,
62
+ periodType: revenueCatData?.periodType,
63
+ });
64
+
65
+ return {
66
+ success: true,
67
+ data: result.finalData ? CreditsMapper.toEntity(result.finalData) : undefined,
68
+ };
69
+ } catch (e: unknown) {
70
+ const message = e instanceof Error ? e.message : String(e);
71
+ return { success: false, error: { message, code: "INIT_ERR" } };
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Delegates to DeductCreditsCommand (Command Pattern)
77
+ */
78
+ async deductCredit(userId: string, cost: number = 1): Promise<DeductCreditsResult> {
79
+ return this.deductCommand.execute(userId, cost);
80
+ }
81
+
82
+ async hasCredits(userId: string, cost: number = 1): Promise<boolean> {
83
+ const res = await this.getCredits(userId);
84
+ return !!(res.success && res.data && res.data.credits >= cost);
85
+ }
86
+
87
+ async syncExpiredStatus(userId: string): Promise<void> {
88
+ const db = getFirestore();
89
+ if (!db) return;
90
+ try {
91
+ await updateDoc(this.getRef(db, userId), {
92
+ isPremium: false,
93
+ status: "expired",
94
+ lastUpdatedAt: serverTimestamp()
95
+ });
96
+ } catch (e) {
97
+ if (__DEV__) console.error("[CreditsRepository] Sync expired failed:", e);
98
+ }
99
+ }
100
+ }
101
+
102
+ export const createCreditsRepository = (c: CreditsConfig) => new CreditsRepository(c);
@@ -5,15 +5,16 @@
5
5
  * Uses status-based state management for reliable loading detection.
6
6
  */
7
7
 
8
- import { useQuery } from "@umituz/react-native-design-system";
9
- import { useCallback, useMemo } from "react";
8
+ import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
9
+ import { useCallback, useMemo, useEffect } from "react";
10
10
  import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
11
- import type { UserCredits } from "../../domain/entities/Credits";
11
+ import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
12
+ import type { UserCredits } from "../core/Credits";
12
13
  import {
13
14
  getCreditsRepository,
14
15
  getCreditsConfig,
15
16
  isCreditsRepositoryConfigured,
16
- } from "../../infrastructure/repositories/CreditsRepositoryProvider";
17
+ } from "../infrastructure/CreditsRepositoryProvider";
17
18
 
18
19
  export const creditsQueryKeys = {
19
20
  all: ["credits"] as const,
@@ -90,6 +91,22 @@ export const useCredits = (): UseCreditsResult => {
90
91
  refetchOnReconnect: true,
91
92
  });
92
93
 
94
+ const queryClient = useQueryClient();
95
+
96
+ // Observer Pattern: Listen for credit updates
97
+ useEffect(() => {
98
+ if (!userId) return;
99
+
100
+ const unsubscribe = subscriptionEventBus.on(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, (updatedUserId) => {
101
+ if (updatedUserId === userId) {
102
+ if (__DEV__) console.log("[useCredits] Event received: CREDITS_UPDATED, refetching...");
103
+ queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
104
+ }
105
+ });
106
+
107
+ return unsubscribe;
108
+ }, [userId, queryClient]);
109
+
93
110
  const credits = data ?? null;
94
111
 
95
112
  const derivedValues = useMemo(() => {
@@ -0,0 +1,26 @@
1
+ import type { FirebaseAuthLike } from "./SubscriptionInitializerTypes";
2
+
3
+ /**
4
+ * Gets the current user ID from Firebase auth.
5
+ */
6
+ export const getCurrentUserId = (getAuth: () => FirebaseAuthLike | null): string | undefined => {
7
+ const auth = getAuth();
8
+ if (!auth) return undefined;
9
+ return auth.currentUser?.uid;
10
+ };
11
+
12
+ /**
13
+ * Sets up auth state listener that will re-initialize subscription
14
+ * when user auth state changes (login/logout).
15
+ */
16
+ export const setupAuthStateListener = (
17
+ getAuth: () => FirebaseAuthLike | null,
18
+ onUserChange: (userId: string | undefined) => void
19
+ ): (() => void) | null => {
20
+ const auth = getAuth();
21
+ if (!auth) return null;
22
+
23
+ return auth.onAuthStateChanged((user) => {
24
+ onUserChange(user?.uid);
25
+ });
26
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Subscription Initializer
3
+ *
4
+ * Uses RevenueCat best practices:
5
+ * - Non-blocking initialization (fire and forget)
6
+ * - Relies on CustomerInfoUpdateListener for state updates
7
+ */
8
+
9
+ import { Platform } from "react-native";
10
+ import { configureCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryProvider";
11
+ import { SubscriptionManager } from "../infrastructure/managers/SubscriptionManager";
12
+ import { configureAuthProvider } from "../presentation/useAuthAwarePurchase";
13
+ import { SubscriptionSyncService } from "./SubscriptionSyncService";
14
+ import { getCurrentUserId, setupAuthStateListener } from "./SubscriptionAuthListener";
15
+ import type { SubscriptionInitConfig } from "./SubscriptionInitializerTypes";
16
+
17
+ export type { FirebaseAuthLike, CreditPackageConfig, SubscriptionInitConfig } from "./SubscriptionInitializerTypes";
18
+
19
+ export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<void> => {
20
+ const {
21
+ apiKey, apiKeyIos, apiKeyAndroid, entitlementId, credits,
22
+ getAnonymousUserId, getFirebaseAuth, showAuthModal,
23
+ onCreditsUpdated, creditPackages,
24
+ } = config;
25
+
26
+ const key = Platform.OS === 'ios' ? (apiKeyIos || apiKey || '') : (apiKeyAndroid || apiKey || '');
27
+ if (!key) throw new Error('API key required');
28
+
29
+ // 1. Configure Repository
30
+ configureCreditsRepository({ ...credits, creditPackageAmounts: creditPackages?.amounts });
31
+
32
+ // 2. Setup Sync Service
33
+ const syncService = new SubscriptionSyncService(entitlementId);
34
+
35
+ // 3. Configure Subscription Manager
36
+ SubscriptionManager.configure({
37
+ config: {
38
+ apiKey: key,
39
+ entitlementIdentifier: entitlementId,
40
+ consumableProductIdentifiers: [creditPackages?.identifierPattern || 'credit'],
41
+ onPurchaseCompleted: (u: string, p: string, c: any, s: any) => syncService.handlePurchase(u, p, c, s),
42
+ onRenewalDetected: (u: string, p: string, expires: string, c: any) => syncService.handleRenewal(u, p, expires, c),
43
+ onPremiumStatusChanged: (u: string, isP: boolean, pId: any, exp: any, willR: any, pt: any) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt),
44
+ onCreditsUpdated,
45
+ },
46
+ apiKey: key,
47
+ getAnonymousUserId,
48
+ });
49
+
50
+ // 4. Configure Auth aware actions
51
+ configureAuthProvider({
52
+ isAuthenticated: () => {
53
+ const u = getFirebaseAuth()?.currentUser;
54
+ return !!(u && !u.isAnonymous);
55
+ },
56
+ showAuthModal,
57
+ });
58
+
59
+ const initializeInBackground = async (userId?: string) => {
60
+ try {
61
+ await SubscriptionManager.initialize(userId);
62
+ if (__DEV__) console.log('[SubscriptionInitializer] Background init complete');
63
+ } catch (error) {
64
+ if (__DEV__) console.log('[SubscriptionInitializer] Background init failed (non-critical):', error);
65
+ }
66
+ };
67
+
68
+ // 5. Start Background Init
69
+ const initialUserId = getCurrentUserId(getFirebaseAuth);
70
+ initializeInBackground(initialUserId);
71
+
72
+ // 6. Listen for Auth Changes
73
+ setupAuthStateListener(getFirebaseAuth, (newUserId) => {
74
+ if (__DEV__) console.log('[SubscriptionInitializer] Auth changed, re-init:', newUserId);
75
+ initializeInBackground(newUserId);
76
+ });
77
+ };
@@ -2,7 +2,10 @@
2
2
  * Subscription Initializer Types
3
3
  */
4
4
 
5
- import type { CreditsConfig } from "../../domain/entities/Credits";
5
+ import type { CreditsConfig } from "../../credits/core/Credits";
6
+ import type { UserCreditsDocumentRead } from "../../credits/core/UserCreditsDocument";
7
+ import type { PurchaseSource, PurchaseType } from "../core/SubscriptionConstants";
8
+ import type { PeriodType } from "../core/SubscriptionStatus";
6
9
 
7
10
  export interface FirebaseAuthLike {
8
11
  currentUser: { uid: string; isAnonymous: boolean } | null;
@@ -28,3 +31,20 @@ export interface SubscriptionInitConfig {
28
31
  timeoutMs?: number;
29
32
  authStateTimeoutMs?: number;
30
33
  }
34
+
35
+ export interface InitializeCreditsMetadata {
36
+ productId?: string;
37
+ source?: PurchaseSource;
38
+ type?: PurchaseType;
39
+ expirationDate?: string | null;
40
+ willRenew?: boolean;
41
+ originalTransactionId?: string;
42
+ isPremium?: boolean;
43
+ periodType?: PeriodType;
44
+ }
45
+
46
+ export interface InitializationResult {
47
+ credits: number;
48
+ alreadyProcessed?: boolean;
49
+ finalData?: UserCreditsDocumentRead;
50
+ }