@umituz/react-native-subscription 2.37.104 → 2.37.105
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/DeductCreditsCommand.ts +1 -2
- package/src/domains/credits/application/PurchaseMetadataGenerator.ts +4 -1
- package/src/domains/credits/application/creditDocumentHelpers.ts +10 -1
- package/src/domains/credits/core/CreditsConstants.ts +3 -0
- package/src/domains/revenuecat/infrastructure/services/UserSwitchMutex.ts +4 -2
- package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +3 -4
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +2 -2
- package/src/domains/subscription/core/SubscriptionConstants.ts +2 -0
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +1 -2
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.types.ts +4 -0
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx +3 -3
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.105",
|
|
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,6 +1,6 @@
|
|
|
1
1
|
import { runTransaction, serverTimestamp, type Transaction, type DocumentReference, type Firestore } from "@umituz/react-native-firebase";
|
|
2
2
|
import type { DeductCreditsResult } from "../core/Credits";
|
|
3
|
-
import { CREDIT_ERROR_CODES } from "../core/CreditsConstants";
|
|
3
|
+
import { CREDIT_ERROR_CODES, MAX_SINGLE_DEDUCTION } from "../core/CreditsConstants";
|
|
4
4
|
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
5
5
|
|
|
6
6
|
export async function deductCreditsOperation(
|
|
@@ -20,7 +20,6 @@ export async function deductCreditsOperation(
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const MAX_SINGLE_DEDUCTION = 10000;
|
|
24
23
|
if (cost <= 0 || !Number.isFinite(cost) || cost > MAX_SINGLE_DEDUCTION) {
|
|
25
24
|
return {
|
|
26
25
|
success: false,
|
|
@@ -26,7 +26,10 @@ export function generatePurchaseMetadata(
|
|
|
26
26
|
const packageType = detectPackageType(productId);
|
|
27
27
|
let purchaseType: PurchaseType = type;
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
const existingLimit = typeof existingData.creditLimit === 'number' && Number.isFinite(existingData.creditLimit)
|
|
30
|
+
? existingData.creditLimit
|
|
31
|
+
: 0;
|
|
32
|
+
if (packageType !== PACKAGE_TYPE.UNKNOWN && creditLimit > existingLimit) {
|
|
30
33
|
purchaseType = PURCHASE_TYPE.UPGRADE;
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -7,7 +7,16 @@ export function getCreditDocumentOrDefault(
|
|
|
7
7
|
platform: Platform
|
|
8
8
|
): UserCreditsDocumentRead {
|
|
9
9
|
if (creditsDoc.exists()) {
|
|
10
|
-
|
|
10
|
+
const raw = creditsDoc.data() as Record<string, unknown>;
|
|
11
|
+
// Ensure critical fields have safe defaults to prevent NaN/undefined propagation
|
|
12
|
+
return {
|
|
13
|
+
...raw,
|
|
14
|
+
credits: typeof raw.credits === 'number' && Number.isFinite(raw.credits) ? raw.credits : 0,
|
|
15
|
+
creditLimit: typeof raw.creditLimit === 'number' && Number.isFinite(raw.creditLimit) ? raw.creditLimit : 0,
|
|
16
|
+
processedPurchases: Array.isArray(raw.processedPurchases) ? raw.processedPurchases : [],
|
|
17
|
+
purchaseHistory: Array.isArray(raw.purchaseHistory) ? raw.purchaseHistory : [],
|
|
18
|
+
isPremium: typeof raw.isPremium === 'boolean' ? raw.isPremium : false,
|
|
19
|
+
} as UserCreditsDocumentRead;
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
const now = serverTimestamp() as unknown as FirestoreTimestamp;
|
|
@@ -12,6 +12,9 @@ export const PURCHASE_ID_PREFIXES = {
|
|
|
12
12
|
|
|
13
13
|
export const PROCESSED_PURCHASES_WINDOW = 50;
|
|
14
14
|
|
|
15
|
+
/** Maximum credits that can be deducted in a single operation. */
|
|
16
|
+
export const MAX_SINGLE_DEDUCTION = 10000;
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
19
|
* Global Firestore collection for cross-user transaction deduplication.
|
|
17
20
|
* Prevents the same Apple/Google transaction from allocating credits
|
|
@@ -18,8 +18,10 @@ class UserSwitchMutexImpl {
|
|
|
18
18
|
}
|
|
19
19
|
try {
|
|
20
20
|
await this.activeSwitchPromise;
|
|
21
|
-
} catch (
|
|
22
|
-
//
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// Previous switch failed — this is non-fatal for the current switch,
|
|
23
|
+
// but worth logging so the failure is visible in diagnostics.
|
|
24
|
+
console.warn('[UserSwitchMutex] Previous user switch failed:', error instanceof Error ? error.message : String(error));
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const timeSinceLastSwitch = Date.now() - this.lastSwitchTime;
|
|
@@ -5,11 +5,9 @@ import type { InitializerDeps } from "./RevenueCatInitializer.types";
|
|
|
5
5
|
import { FAILED_INITIALIZATION_RESULT } from "./initializerConstants";
|
|
6
6
|
import { UserSwitchMutex } from "./UserSwitchMutex";
|
|
7
7
|
import { getPremiumEntitlement } from "../../core/types";
|
|
8
|
-
import type
|
|
8
|
+
import { ANONYMOUS_CACHE_KEY, type PeriodType } from "../../../subscription/core/SubscriptionConstants";
|
|
9
9
|
import { requireFirestore } from "../../../../shared/infrastructure/firestore";
|
|
10
10
|
|
|
11
|
-
const ANONYMOUS_CACHE_KEY = '__anonymous__';
|
|
12
|
-
|
|
13
11
|
declare const __DEV__: boolean;
|
|
14
12
|
|
|
15
13
|
function buildSuccessResult(deps: InitializerDeps, customerInfo: CustomerInfo, offerings: PurchasesOfferings | null): InitializeResult {
|
|
@@ -46,8 +44,9 @@ async function syncRevenueCatIdToProfile(firebaseUserId: string, revenueCatUserI
|
|
|
46
44
|
const db = requireFirestore();
|
|
47
45
|
const userRef = doc(db, "users", firebaseUserId);
|
|
48
46
|
await setDoc(userRef, { revenueCatUserId }, { merge: true });
|
|
49
|
-
} catch {
|
|
47
|
+
} catch (error) {
|
|
50
48
|
// Non-fatal: profile update failure should not block SDK initialization
|
|
49
|
+
console.warn('[UserSwitchHandler] Failed to sync RevenueCat ID to profile:', error instanceof Error ? error.message : String(error));
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
52
|
|
|
@@ -27,14 +27,14 @@ export class SubscriptionSyncProcessor {
|
|
|
27
27
|
|
|
28
28
|
private async getCreditsUserId(revenueCatUserId: string | null | undefined): Promise<string> {
|
|
29
29
|
const trimmed = revenueCatUserId?.trim();
|
|
30
|
-
if (trimmed && trimmed.length > 0) {
|
|
30
|
+
if (trimmed && trimmed.length > 0 && trimmed !== 'undefined' && trimmed !== 'null') {
|
|
31
31
|
return trimmed;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
console.warn("[SubscriptionSyncProcessor] revenueCatUserId is empty/null, falling back to anonymousUserId");
|
|
35
35
|
const anonymousId = await this.getAnonymousUserId();
|
|
36
36
|
const trimmedAnonymous = anonymousId?.trim();
|
|
37
|
-
if (!trimmedAnonymous || trimmedAnonymous.length === 0) {
|
|
37
|
+
if (!trimmedAnonymous || trimmedAnonymous.length === 0 || trimmedAnonymous === 'undefined' || trimmedAnonymous === 'null') {
|
|
38
38
|
throw new Error("[SubscriptionSyncProcessor] Cannot resolve credits userId: both revenueCatUserId and anonymousUserId are empty");
|
|
39
39
|
}
|
|
40
40
|
return trimmedAnonymous;
|
|
@@ -9,8 +9,7 @@ import { checkPremiumStatusFromService } from "./premiumStatusChecker";
|
|
|
9
9
|
import { getPackagesOperation, purchasePackageOperation, restoreOperation } from "./managerOperations";
|
|
10
10
|
import { performServiceInitialization } from "./initializationHandler";
|
|
11
11
|
import { initializationState } from "../state/initializationState";
|
|
12
|
-
|
|
13
|
-
const ANONYMOUS_CACHE_KEY = '__anonymous__';
|
|
12
|
+
import { ANONYMOUS_CACHE_KEY } from "../../core/SubscriptionConstants";
|
|
14
13
|
|
|
15
14
|
class SubscriptionManagerImpl {
|
|
16
15
|
private managerConfig: SubscriptionManagerConfig | null = null;
|
package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.types.ts
CHANGED
|
@@ -25,6 +25,10 @@ export interface SubscriptionHeaderProps {
|
|
|
25
25
|
latestPurchaseDateLabel?: string;
|
|
26
26
|
billingIssuesLabel?: string;
|
|
27
27
|
sandboxLabel?: string;
|
|
28
|
+
willRenewYes?: string;
|
|
29
|
+
willRenewNo?: string;
|
|
30
|
+
billingIssuesDetected?: string;
|
|
31
|
+
sandboxTestMode?: string;
|
|
28
32
|
};
|
|
29
33
|
// Additional RevenueCat subscription details
|
|
30
34
|
willRenew?: boolean | null;
|
package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx
CHANGED
|
@@ -67,7 +67,7 @@ export const SubscriptionHeaderContent: React.FC<SubscriptionHeaderContentProps>
|
|
|
67
67
|
{willRenew !== null && willRenew !== undefined && translations.willRenewLabel && (
|
|
68
68
|
<DetailRow
|
|
69
69
|
label={translations.willRenewLabel}
|
|
70
|
-
value={willRenew ? "Yes" : "No"}
|
|
70
|
+
value={willRenew ? (translations.willRenewYes ?? "Yes") : (translations.willRenewNo ?? "No")}
|
|
71
71
|
highlight={!willRenew}
|
|
72
72
|
style={styles.row}
|
|
73
73
|
labelStyle={styles.label}
|
|
@@ -113,7 +113,7 @@ export const SubscriptionHeaderContent: React.FC<SubscriptionHeaderContentProps>
|
|
|
113
113
|
{billingIssuesDetected && translations.billingIssuesLabel && (
|
|
114
114
|
<DetailRow
|
|
115
115
|
label={translations.billingIssuesLabel}
|
|
116
|
-
value="Detected"
|
|
116
|
+
value={translations.billingIssuesDetected ?? "Detected"}
|
|
117
117
|
highlight={true}
|
|
118
118
|
style={styles.row}
|
|
119
119
|
labelStyle={styles.label}
|
|
@@ -123,7 +123,7 @@ export const SubscriptionHeaderContent: React.FC<SubscriptionHeaderContentProps>
|
|
|
123
123
|
{typeof __DEV__ !== 'undefined' && __DEV__ && isSandbox && translations.sandboxLabel && (
|
|
124
124
|
<DetailRow
|
|
125
125
|
label={translations.sandboxLabel}
|
|
126
|
-
value="Test Mode"
|
|
126
|
+
value={translations.sandboxTestMode ?? "Test Mode"}
|
|
127
127
|
style={styles.row}
|
|
128
128
|
labelStyle={styles.label}
|
|
129
129
|
valueStyle={styles.value}
|