@umituz/react-native-subscription 2.37.36 → 2.37.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.37.36",
3
+ "version": "2.37.38",
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",
@@ -7,6 +7,7 @@ import { getCreditDocumentOrDefault } from "./creditDocumentHelpers";
7
7
  import { calculateNewCredits, buildCreditsData, shouldSkipStatusSyncWrite } from "./creditOperationUtils";
8
8
  import { calculateCreditLimit } from "./CreditLimitCalculator";
9
9
  import { generatePurchaseMetadata } from "./PurchaseMetadataGenerator";
10
+ import { PURCHASE_ID_PREFIXES } from "../core/CreditsConstants";
10
11
 
11
12
  export async function initializeCreditsTransaction(
12
13
  _db: Firestore,
@@ -21,6 +22,18 @@ export async function initializeCreditsTransaction(
21
22
 
22
23
  return runTransaction(async (transaction: Transaction) => {
23
24
  const creditsDoc = await transaction.get(creditsRef);
25
+
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
+
24
37
  const existingData = getCreditDocumentOrDefault(creditsDoc, platform);
25
38
 
26
39
  if (existingData.processedPurchases.includes(purchaseId)) {
@@ -1,8 +1,9 @@
1
1
  import { ICreditStrategy, type CreditAllocationParams } from "./ICreditStrategy";
2
2
 
3
3
  /**
4
- * Strategy for existing premium users during a simple status synchronization.
5
- * Preserves their current credits to avoid accidental resets.
4
+ * Strategy for status synchronization (app open, auth state changes).
5
+ * ONLY preserves existing credits. NEVER allocates new credits.
6
+ * New credits are ONLY allocated by purchase and renewal flows.
6
7
  */
7
8
  export class SyncCreditStrategy implements ICreditStrategy {
8
9
  canHandle(params: CreditAllocationParams): boolean {
@@ -10,18 +11,14 @@ export class SyncCreditStrategy implements ICreditStrategy {
10
11
  }
11
12
 
12
13
  execute(params: CreditAllocationParams): number {
14
+ // Status sync only preserves existing credits, never allocates new ones
13
15
  const existingCredits = params.existingData?.credits;
14
- const hasExistingCredits = typeof existingCredits === 'number' && existingCredits >= 0;
15
- const hasExistingDocument = (params.existingData?.processedPurchases?.length ?? 0) > 0;
16
-
17
- if (params.isSubscriptionActive && !hasExistingDocument) {
18
- return params.creditLimit;
19
- }
20
-
21
- if (hasExistingCredits && params.existingData) {
22
- return params.existingData.credits;
16
+ if (typeof existingCredits === 'number' && existingCredits >= 0) {
17
+ return existingCredits;
23
18
  }
24
19
 
25
- return params.creditLimit;
20
+ // No existing credits = no document = return 0
21
+ // Credits are only allocated through purchase/renewal flows
22
+ return 0;
26
23
  }
27
24
  }
@@ -8,6 +8,6 @@ export async function syncExpiredStatus(ref: DocumentReference): Promise<void> {
8
8
  isPremium: false,
9
9
  status: SUBSCRIPTION_STATUS.EXPIRED,
10
10
  willRenew: false,
11
- expirationDate: serverTimestamp(),
11
+ lastUpdatedAt: serverTimestamp(),
12
12
  }, { merge: true });
13
13
  }
@@ -3,7 +3,7 @@ import { timezoneService } from "@umituz/react-native-design-system";
3
3
  import type { UserCredits, DeductCreditsResult } from "../../core/Credits";
4
4
  import type { CreditsRepository } from "../../infrastructure/CreditsRepository";
5
5
  import { creditsQueryKeys } from "../creditsQueryKeys";
6
- import { calculateRemaining } from "../../../../shared/utils/numberUtils";
6
+ import { calculateRemaining } from "../../../../shared/utils/numberUtils.core";
7
7
 
8
8
  export interface MutationContext {
9
9
  previousCredits: UserCredits | null;
@@ -5,8 +5,7 @@ import { getCreditsRepository } from "../../credits/infrastructure/CreditsReposi
5
5
  import { extractRevenueCatData } from "./SubscriptionSyncUtils";
6
6
  import { emitCreditsUpdated } from "./syncEventEmitter";
7
7
  import { generatePurchaseId, generateRenewalId } from "./syncIdGenerators";
8
- import { handleExpiredSubscription, handleFreeUserInitialization, handlePremiumStatusSync } from "./statusChangeHandlers";
9
- import { NO_SUBSCRIPTION_PRODUCT_ID } from "./syncConstants";
8
+ import { handleExpiredSubscription, handlePremiumStatusSync } from "./statusChangeHandlers";
10
9
  import type { PackageType } from "../../revenuecat/core/types";
11
10
 
12
11
  export class SubscriptionSyncProcessor {
@@ -76,17 +75,21 @@ export class SubscriptionSyncProcessor {
76
75
  return;
77
76
  }
78
77
 
79
- // Free user case
78
+ // Free user case - no Firestore document needed
79
+ // Credits absence means no subscription (app handles null gracefully)
80
80
  if (!isPremium && !productId) {
81
- await handleFreeUserInitialization(creditsUserId);
82
81
  return;
83
82
  }
84
83
 
85
- // Premium user case
84
+ // Premium user case - productId is required
85
+ if (!productId) {
86
+ return;
87
+ }
88
+
86
89
  await handlePremiumStatusSync(
87
90
  creditsUserId,
88
91
  isPremium,
89
- productId ?? NO_SUBSCRIPTION_PRODUCT_ID,
92
+ productId,
90
93
  expiresAt ?? null,
91
94
  willRenew ?? false,
92
95
  periodType ?? null
@@ -3,27 +3,13 @@ import type { PeriodType } from "../core/SubscriptionConstants";
3
3
  import { PURCHASE_SOURCE, PURCHASE_TYPE } from "../core/SubscriptionConstants";
4
4
  import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
5
5
  import { emitCreditsUpdated } from "./syncEventEmitter";
6
- import { generateInitSyncId, generateStatusSyncId } from "./syncIdGenerators";
7
- import { NO_SUBSCRIPTION_PRODUCT_ID, DEFAULT_FREE_USER_DATA } from "./syncConstants";
6
+ import { generateStatusSyncId } from "./syncIdGenerators";
8
7
 
9
8
  export const handleExpiredSubscription = async (userId: string): Promise<void> => {
10
9
  await getCreditsRepository().syncExpiredStatus(userId);
11
10
  emitCreditsUpdated(userId);
12
11
  };
13
12
 
14
- export const handleFreeUserInitialization = async (userId: string): Promise<void> => {
15
- const stableSyncId = generateInitSyncId(userId);
16
- await getCreditsRepository().initializeCredits(
17
- userId,
18
- stableSyncId,
19
- NO_SUBSCRIPTION_PRODUCT_ID,
20
- PURCHASE_SOURCE.SETTINGS,
21
- DEFAULT_FREE_USER_DATA,
22
- PURCHASE_TYPE.INITIAL
23
- );
24
- emitCreditsUpdated(userId);
25
- };
26
-
27
13
  export const handlePremiumStatusSync = async (
28
14
  userId: string,
29
15
  isPremium: boolean,
@@ -1,16 +1 @@
1
- import type { RevenueCatData } from "../../revenuecat/core/types";
2
-
3
1
  export const NO_SUBSCRIPTION_PRODUCT_ID = 'no_subscription';
4
-
5
- export const DEFAULT_FREE_USER_DATA: RevenueCatData = {
6
- isPremium: false,
7
- expirationDate: null,
8
- willRenew: false,
9
- periodType: null,
10
- packageType: null,
11
- originalTransactionId: null,
12
- unsubscribeDetectedAt: null,
13
- billingIssueDetectedAt: null,
14
- store: null,
15
- ownershipType: null,
16
- };
@@ -10,10 +10,6 @@ export const generateRenewalId = (originalTransactionId: string | null, productI
10
10
  : `renewal_${productId}_${Date.now()}`;
11
11
  };
12
12
 
13
- export const generateInitSyncId = (userId: string): string => {
14
- return `init_sync_${userId}`;
15
- };
16
-
17
13
  export const generateStatusSyncId = (userId: string, isPremium: boolean): string => {
18
14
  return `status_sync_${userId}_${isPremium ? 'premium' : 'free'}`;
19
15
  };
@@ -1 +0,0 @@
1
- export { calculateRemaining } from "./numberUtils.core";