@umituz/react-native-subscription 2.37.38 → 2.37.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/credits/application/CreditsInitializer.ts +5 -20
- package/src/domains/credits/application/credit-strategies/CreditAllocationOrchestrator.ts +1 -9
- package/src/domains/credits/application/credit-strategies/ICreditStrategy.ts +1 -5
- package/src/domains/credits/application/creditOperationUtils.ts +1 -43
- package/src/domains/credits/core/CreditsConstants.ts +0 -11
- package/src/domains/credits/infrastructure/CreditsRepository.ts +6 -1
- package/src/domains/credits/infrastructure/operations/CreditsWriter.ts +51 -1
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +0 -4
- package/src/domains/subscription/application/SubscriptionSyncService.ts +4 -17
- package/src/domains/subscription/application/statusChangeHandlers.ts +14 -27
- package/src/domains/subscription/application/syncIdGenerators.ts +0 -4
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +1 -10
- package/src/domains/credits/application/credit-strategies/SyncCreditStrategy.ts +0 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.37.
|
|
3
|
+
"version": "2.37.39",
|
|
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",
|
|
@@ -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
|
|
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 {
|
|
@@ -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(),
|
|
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
|
|
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;
|
|
@@ -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,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;
|
|
@@ -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,14 @@
|
|
|
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";
|
|
5
7
|
|
|
6
8
|
export async function syncExpiredStatus(ref: DocumentReference): Promise<void> {
|
|
9
|
+
const doc = await getDoc(ref);
|
|
10
|
+
if (!doc.exists()) return;
|
|
11
|
+
|
|
7
12
|
await setDoc(ref, {
|
|
8
13
|
isPremium: false,
|
|
9
14
|
status: SUBSCRIPTION_STATUS.EXPIRED,
|
|
@@ -11,3 +16,48 @@ export async function syncExpiredStatus(ref: DocumentReference): Promise<void> {
|
|
|
11
16
|
lastUpdatedAt: serverTimestamp(),
|
|
12
17
|
}, { merge: true });
|
|
13
18
|
}
|
|
19
|
+
|
|
20
|
+
export interface PremiumMetadata {
|
|
21
|
+
isPremium: boolean;
|
|
22
|
+
willRenew: boolean;
|
|
23
|
+
expirationDate: string | null;
|
|
24
|
+
productId: string;
|
|
25
|
+
periodType: string | null;
|
|
26
|
+
unsubscribeDetectedAt: string | null;
|
|
27
|
+
billingIssueDetectedAt: string | null;
|
|
28
|
+
store: string | null;
|
|
29
|
+
ownershipType: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function syncPremiumMetadata(
|
|
33
|
+
ref: DocumentReference,
|
|
34
|
+
metadata: PremiumMetadata
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
const doc = await getDoc(ref);
|
|
37
|
+
if (!doc.exists()) return;
|
|
38
|
+
|
|
39
|
+
const isExpired = false;
|
|
40
|
+
const status = resolveSubscriptionStatus({
|
|
41
|
+
isPremium: metadata.isPremium,
|
|
42
|
+
willRenew: metadata.willRenew,
|
|
43
|
+
isExpired,
|
|
44
|
+
periodType: metadata.periodType ?? undefined,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const expirationTimestamp = metadata.expirationDate ? toTimestamp(metadata.expirationDate) : null;
|
|
48
|
+
const canceledAtTimestamp = metadata.unsubscribeDetectedAt ? toTimestamp(metadata.unsubscribeDetectedAt) : null;
|
|
49
|
+
const billingIssueTimestamp = metadata.billingIssueDetectedAt ? toTimestamp(metadata.billingIssueDetectedAt) : null;
|
|
50
|
+
|
|
51
|
+
await setDoc(ref, {
|
|
52
|
+
isPremium: metadata.isPremium,
|
|
53
|
+
status,
|
|
54
|
+
willRenew: metadata.willRenew,
|
|
55
|
+
productId: metadata.productId,
|
|
56
|
+
lastUpdatedAt: serverTimestamp(),
|
|
57
|
+
...(expirationTimestamp && { expirationDate: expirationTimestamp }),
|
|
58
|
+
...(canceledAtTimestamp && { canceledAt: canceledAtTimestamp }),
|
|
59
|
+
...(billingIssueTimestamp && { billingIssueDetectedAt: billingIssueTimestamp }),
|
|
60
|
+
...(metadata.store && { store: metadata.store }),
|
|
61
|
+
...(metadata.ownershipType && { ownershipType: metadata.ownershipType }),
|
|
62
|
+
}, { merge: true });
|
|
63
|
+
}
|
|
@@ -69,19 +69,15 @@ export class SubscriptionSyncProcessor {
|
|
|
69
69
|
) {
|
|
70
70
|
const creditsUserId = await this.getCreditsUserId(userId);
|
|
71
71
|
|
|
72
|
-
// Expired subscription case
|
|
73
72
|
if (!isPremium && productId) {
|
|
74
73
|
await handleExpiredSubscription(creditsUserId);
|
|
75
74
|
return;
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
// Free user case - no Firestore document needed
|
|
79
|
-
// Credits absence means no subscription (app handles null gracefully)
|
|
80
77
|
if (!isPremium && !productId) {
|
|
81
78
|
return;
|
|
82
79
|
}
|
|
83
80
|
|
|
84
|
-
// Premium user case - productId is required
|
|
85
81
|
if (!productId) {
|
|
86
82
|
return;
|
|
87
83
|
}
|
|
@@ -15,17 +15,8 @@ export class SubscriptionSyncService {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
async handlePurchase(userId: string, productId: string, customerInfo: CustomerInfo, source?: PurchaseSource, packageType?: PackageType | null) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PURCHASE_COMPLETED, { userId, productId });
|
|
21
|
-
} catch (error) {
|
|
22
|
-
console.error('[SubscriptionSyncService] Purchase processing failed', {
|
|
23
|
-
userId,
|
|
24
|
-
productId,
|
|
25
|
-
source,
|
|
26
|
-
error
|
|
27
|
-
});
|
|
28
|
-
}
|
|
18
|
+
await this.processor.processPurchase(userId, productId, customerInfo, source, packageType);
|
|
19
|
+
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PURCHASE_COMPLETED, { userId, productId });
|
|
29
20
|
}
|
|
30
21
|
|
|
31
22
|
async handleRenewal(userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) {
|
|
@@ -37,7 +28,7 @@ export class SubscriptionSyncService {
|
|
|
37
28
|
userId,
|
|
38
29
|
productId,
|
|
39
30
|
newExpirationDate,
|
|
40
|
-
error
|
|
31
|
+
error: error instanceof Error ? error.message : String(error)
|
|
41
32
|
});
|
|
42
33
|
}
|
|
43
34
|
}
|
|
@@ -58,12 +49,8 @@ export class SubscriptionSyncService {
|
|
|
58
49
|
userId,
|
|
59
50
|
isPremium,
|
|
60
51
|
productId,
|
|
61
|
-
|
|
62
|
-
willRenew,
|
|
63
|
-
periodType,
|
|
64
|
-
error
|
|
52
|
+
error: error instanceof Error ? error.message : String(error)
|
|
65
53
|
});
|
|
66
54
|
}
|
|
67
55
|
}
|
|
68
56
|
}
|
|
69
|
-
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import type { RevenueCatData } from "../../revenuecat/core/types";
|
|
2
1
|
import type { PeriodType } from "../core/SubscriptionConstants";
|
|
3
|
-
import { PURCHASE_SOURCE, PURCHASE_TYPE } from "../core/SubscriptionConstants";
|
|
4
2
|
import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
|
|
5
3
|
import { emitCreditsUpdated } from "./syncEventEmitter";
|
|
6
|
-
import { generateStatusSyncId } from "./syncIdGenerators";
|
|
7
4
|
|
|
8
5
|
export const handleExpiredSubscription = async (userId: string): Promise<void> => {
|
|
9
6
|
await getCreditsRepository().syncExpiredStatus(userId);
|
|
@@ -16,32 +13,22 @@ export const handlePremiumStatusSync = async (
|
|
|
16
13
|
productId: string,
|
|
17
14
|
expiresAt: string | null,
|
|
18
15
|
willRenew: boolean,
|
|
19
|
-
periodType: PeriodType | null
|
|
16
|
+
periodType: PeriodType | null,
|
|
17
|
+
unsubscribeDetectedAt?: string | null,
|
|
18
|
+
billingIssueDetectedAt?: string | null,
|
|
19
|
+
store?: string | null,
|
|
20
|
+
ownershipType?: string | null
|
|
20
21
|
): Promise<void> => {
|
|
21
|
-
|
|
22
|
-
const revenueCatData: RevenueCatData = {
|
|
23
|
-
expirationDate: expiresAt,
|
|
24
|
-
willRenew,
|
|
22
|
+
await getCreditsRepository().syncPremiumMetadata(userId, {
|
|
25
23
|
isPremium,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
originalTransactionId: null,
|
|
29
|
-
unsubscribeDetectedAt: null,
|
|
30
|
-
billingIssueDetectedAt: null,
|
|
31
|
-
store: null,
|
|
32
|
-
ownershipType: null
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const statusSyncId = generateStatusSyncId(userId, isPremium);
|
|
36
|
-
|
|
37
|
-
await getCreditsRepository().initializeCredits(
|
|
38
|
-
userId,
|
|
39
|
-
statusSyncId,
|
|
24
|
+
willRenew,
|
|
25
|
+
expirationDate: expiresAt,
|
|
40
26
|
productId,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
27
|
+
periodType,
|
|
28
|
+
unsubscribeDetectedAt: unsubscribeDetectedAt ?? null,
|
|
29
|
+
billingIssueDetectedAt: billingIssueDetectedAt ?? null,
|
|
30
|
+
store: store ?? null,
|
|
31
|
+
ownershipType: ownershipType ?? null,
|
|
32
|
+
});
|
|
46
33
|
emitCreditsUpdated(userId);
|
|
47
34
|
};
|
|
@@ -9,7 +9,3 @@ export const generateRenewalId = (originalTransactionId: string | null, productI
|
|
|
9
9
|
? `renewal_${originalTransactionId}_${expirationDate}`
|
|
10
10
|
: `renewal_${productId}_${Date.now()}`;
|
|
11
11
|
};
|
|
12
|
-
|
|
13
|
-
export const generateStatusSyncId = (userId: string, isPremium: boolean): string => {
|
|
14
|
-
return `status_sync_${userId}_${isPremium ? 'premium' : 'free'}`;
|
|
15
|
-
};
|
|
@@ -70,16 +70,7 @@ export async function notifyPurchaseCompleted(
|
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
await config.onPurchaseCompleted(userId, productId, customerInfo, source, packageType);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
// Silently fail callback notifications to prevent crashing the main flow
|
|
77
|
-
console.error('[PremiumStatusSyncer] Purchase completed callback failed:', {
|
|
78
|
-
userId,
|
|
79
|
-
productId,
|
|
80
|
-
error: error instanceof Error ? error.message : String(error)
|
|
81
|
-
});
|
|
82
|
-
}
|
|
73
|
+
await config.onPurchaseCompleted(userId, productId, customerInfo, source, packageType);
|
|
83
74
|
}
|
|
84
75
|
|
|
85
76
|
export async function notifyRestoreCompleted(
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { ICreditStrategy, type CreditAllocationParams } from "./ICreditStrategy";
|
|
2
|
-
|
|
3
|
-
/**
|
|
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.
|
|
7
|
-
*/
|
|
8
|
-
export class SyncCreditStrategy implements ICreditStrategy {
|
|
9
|
-
canHandle(params: CreditAllocationParams): boolean {
|
|
10
|
-
return params.isStatusSync;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
execute(params: CreditAllocationParams): number {
|
|
14
|
-
// Status sync only preserves existing credits, never allocates new ones
|
|
15
|
-
const existingCredits = params.existingData?.credits;
|
|
16
|
-
if (typeof existingCredits === 'number' && existingCredits >= 0) {
|
|
17
|
-
return existingCredits;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// No existing credits = no document = return 0
|
|
21
|
-
// Credits are only allocated through purchase/renewal flows
|
|
22
|
-
return 0;
|
|
23
|
-
}
|
|
24
|
-
}
|