@umituz/react-native-subscription 2.35.4 → 2.35.6
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/revenuecat/core/types/RevenueCatConfig.ts +3 -1
- package/src/domains/revenuecat/core/types/RevenueCatData.ts +3 -2
- package/src/domains/revenuecat/core/types/RevenueCatTypes.ts +8 -1
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +27 -9
- package/src/domains/subscription/application/SubscriptionSyncService.ts +8 -4
- package/src/domains/subscription/application/SubscriptionSyncUtils.ts +2 -0
- package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +10 -10
- package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +3 -3
- package/src/domains/subscription/application/statusChangeHandlers.ts +1 -0
- package/src/domains/subscription/application/syncConstants.ts +1 -0
- package/src/domains/subscription/infrastructure/handlers/PurchaseStatusResolver.ts +3 -0
- package/src/domains/subscription/infrastructure/services/RestoreHandler.ts +1 -4
- package/src/domains/subscription/infrastructure/services/purchase/PurchaseExecutor.ts +15 -15
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +5 -3
- package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.tsx +1 -0
- package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.types.ts +1 -0
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.tsx +2 -0
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.types.ts +1 -0
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx +20 -3
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +1 -0
- package/src/domains/subscription/presentation/useSubscriptionStatus.types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.35.
|
|
3
|
+
"version": "2.35.6",
|
|
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,4 +1,5 @@
|
|
|
1
1
|
import type { CustomerInfo } from "react-native-purchases";
|
|
2
|
+
import type { PackageType } from "./RevenueCatTypes";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* RevenueCat Configuration
|
|
@@ -20,7 +21,8 @@ export interface RevenueCatConfig {
|
|
|
20
21
|
userId: string,
|
|
21
22
|
productId: string,
|
|
22
23
|
customerInfo: CustomerInfo,
|
|
23
|
-
source?: string // Purchase source tracking (app-specific)
|
|
24
|
+
source?: string, // Purchase source tracking (app-specific)
|
|
25
|
+
packageType?: PackageType | null // From PurchasesPackage.packageType - subscription duration (WEEKLY, MONTHLY, ANNUAL, etc.)
|
|
24
26
|
) => Promise<void> | void;
|
|
25
27
|
onRestoreCompleted?: (
|
|
26
28
|
userId: string,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Store, OwnershipType } from "./RevenueCatTypes";
|
|
1
|
+
import type { Store, OwnershipType, PackageType } from "./RevenueCatTypes";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* RevenueCat subscription data (Single Source of Truth)
|
|
@@ -10,7 +10,8 @@ export interface RevenueCatData {
|
|
|
10
10
|
willRenew: boolean | null;
|
|
11
11
|
originalTransactionId: string | null;
|
|
12
12
|
isPremium: boolean;
|
|
13
|
-
periodType: string | null; // From RevenueCat SDK (NORMAL, INTRO, TRIAL)
|
|
13
|
+
periodType: string | null; // From RevenueCat SDK (NORMAL, INTRO, TRIAL) - pricing type
|
|
14
|
+
packageType: PackageType | null; // From PurchasesPackage.packageType - subscription duration (WEEKLY, MONTHLY, ANNUAL, etc.)
|
|
14
15
|
unsubscribeDetectedAt: string | null;
|
|
15
16
|
billingIssueDetectedAt: string | null;
|
|
16
17
|
store: Store | null; // From PurchasesEntitlementInfo['store']
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Proper typing for RevenueCat entitlements and errors
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { CustomerInfo, PurchasesEntitlementInfo } from "react-native-purchases";
|
|
6
|
+
import type { CustomerInfo, PurchasesEntitlementInfo, PurchasesPackage } from "react-native-purchases";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Default entitlement identifier
|
|
@@ -23,6 +23,13 @@ export type Store = PurchasesEntitlementInfo['store'];
|
|
|
23
23
|
*/
|
|
24
24
|
export type OwnershipType = PurchasesEntitlementInfo['ownershipType'];
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* PackageType - Directly from RevenueCat SDK
|
|
28
|
+
* Represents subscription duration (WEEKLY, MONTHLY, ANNUAL, LIFETIME, etc.)
|
|
29
|
+
* Automatically stays in sync with RevenueCat SDK updates
|
|
30
|
+
*/
|
|
31
|
+
export type PackageType = PurchasesPackage['packageType'];
|
|
32
|
+
|
|
26
33
|
/**
|
|
27
34
|
* RevenueCat Entitlement Info
|
|
28
35
|
* Represents active entitlement data from CustomerInfo
|
|
@@ -7,16 +7,30 @@ import { emitCreditsUpdated } from "./syncEventEmitter";
|
|
|
7
7
|
import { generatePurchaseId, generateRenewalId } from "./syncIdGenerators";
|
|
8
8
|
import { handleExpiredSubscription, handleFreeUserInitialization, handlePremiumStatusSync } from "./statusChangeHandlers";
|
|
9
9
|
import { NO_SUBSCRIPTION_PRODUCT_ID } from "./syncConstants";
|
|
10
|
+
import type { PackageType } from "../../revenuecat/core/types";
|
|
10
11
|
|
|
11
12
|
export class SubscriptionSyncProcessor {
|
|
12
|
-
constructor(
|
|
13
|
+
constructor(
|
|
14
|
+
private entitlementId: string,
|
|
15
|
+
private getAnonymousUserId: () => Promise<string>
|
|
16
|
+
) {}
|
|
13
17
|
|
|
14
|
-
async
|
|
18
|
+
private async getCreditsUserId(revenueCatUserId: string): Promise<string> {
|
|
19
|
+
if (!revenueCatUserId || revenueCatUserId.length === 0) {
|
|
20
|
+
return this.getAnonymousUserId();
|
|
21
|
+
}
|
|
22
|
+
return revenueCatUserId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async processPurchase(userId: string, productId: string, customerInfo: CustomerInfo, source?: PurchaseSource, packageType?: PackageType | null) {
|
|
15
26
|
const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
|
|
27
|
+
revenueCatData.packageType = packageType ?? null;
|
|
16
28
|
const purchaseId = generatePurchaseId(revenueCatData.originalTransactionId, productId);
|
|
17
29
|
|
|
30
|
+
const creditsUserId = await this.getCreditsUserId(userId);
|
|
31
|
+
|
|
18
32
|
await getCreditsRepository().initializeCredits(
|
|
19
|
-
|
|
33
|
+
creditsUserId,
|
|
20
34
|
purchaseId,
|
|
21
35
|
productId,
|
|
22
36
|
source ?? PURCHASE_SOURCE.SETTINGS,
|
|
@@ -24,7 +38,7 @@ export class SubscriptionSyncProcessor {
|
|
|
24
38
|
PURCHASE_TYPE.INITIAL
|
|
25
39
|
);
|
|
26
40
|
|
|
27
|
-
emitCreditsUpdated(
|
|
41
|
+
emitCreditsUpdated(creditsUserId);
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
async processRenewal(userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) {
|
|
@@ -32,8 +46,10 @@ export class SubscriptionSyncProcessor {
|
|
|
32
46
|
revenueCatData.expirationDate = newExpirationDate || revenueCatData.expirationDate;
|
|
33
47
|
const purchaseId = generateRenewalId(revenueCatData.originalTransactionId, productId, newExpirationDate);
|
|
34
48
|
|
|
49
|
+
const creditsUserId = await this.getCreditsUserId(userId);
|
|
50
|
+
|
|
35
51
|
await getCreditsRepository().initializeCredits(
|
|
36
|
-
|
|
52
|
+
creditsUserId,
|
|
37
53
|
purchaseId,
|
|
38
54
|
productId,
|
|
39
55
|
PURCHASE_SOURCE.RENEWAL,
|
|
@@ -41,7 +57,7 @@ export class SubscriptionSyncProcessor {
|
|
|
41
57
|
PURCHASE_TYPE.RENEWAL
|
|
42
58
|
);
|
|
43
59
|
|
|
44
|
-
emitCreditsUpdated(
|
|
60
|
+
emitCreditsUpdated(creditsUserId);
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
async processStatusChange(
|
|
@@ -52,21 +68,23 @@ export class SubscriptionSyncProcessor {
|
|
|
52
68
|
willRenew?: boolean,
|
|
53
69
|
periodType?: PeriodType
|
|
54
70
|
) {
|
|
71
|
+
const creditsUserId = await this.getCreditsUserId(userId);
|
|
72
|
+
|
|
55
73
|
// Expired subscription case
|
|
56
74
|
if (!isPremium && productId) {
|
|
57
|
-
await handleExpiredSubscription(
|
|
75
|
+
await handleExpiredSubscription(creditsUserId);
|
|
58
76
|
return;
|
|
59
77
|
}
|
|
60
78
|
|
|
61
79
|
// Free user case
|
|
62
80
|
if (!isPremium && !productId) {
|
|
63
|
-
await handleFreeUserInitialization(
|
|
81
|
+
await handleFreeUserInitialization(creditsUserId);
|
|
64
82
|
return;
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
// Premium user case
|
|
68
86
|
await handlePremiumStatusSync(
|
|
69
|
-
|
|
87
|
+
creditsUserId,
|
|
70
88
|
isPremium,
|
|
71
89
|
productId ?? NO_SUBSCRIPTION_PRODUCT_ID,
|
|
72
90
|
expiresAt ?? null,
|
|
@@ -2,17 +2,21 @@ import type { CustomerInfo } from "react-native-purchases";
|
|
|
2
2
|
import { type PeriodType, type PurchaseSource } from "../core/SubscriptionConstants";
|
|
3
3
|
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
4
4
|
import { SubscriptionSyncProcessor } from "./SubscriptionSyncProcessor";
|
|
5
|
+
import type { PackageType } from "../../revenuecat/core/types";
|
|
5
6
|
|
|
6
7
|
export class SubscriptionSyncService {
|
|
7
8
|
private processor: SubscriptionSyncProcessor;
|
|
8
9
|
|
|
9
|
-
constructor(
|
|
10
|
-
|
|
10
|
+
constructor(
|
|
11
|
+
entitlementId: string,
|
|
12
|
+
getAnonymousUserId: () => Promise<string>
|
|
13
|
+
) {
|
|
14
|
+
this.processor = new SubscriptionSyncProcessor(entitlementId, getAnonymousUserId);
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
async handlePurchase(userId: string, productId: string, customerInfo: CustomerInfo, source?: PurchaseSource) {
|
|
17
|
+
async handlePurchase(userId: string, productId: string, customerInfo: CustomerInfo, source?: PurchaseSource, packageType?: PackageType | null) {
|
|
14
18
|
try {
|
|
15
|
-
await this.processor.processPurchase(userId, productId, customerInfo, source);
|
|
19
|
+
await this.processor.processPurchase(userId, productId, customerInfo, source, packageType);
|
|
16
20
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PURCHASE_COMPLETED, { userId, productId });
|
|
17
21
|
} catch (error) {
|
|
18
22
|
console.error('[SubscriptionSyncService] Purchase processing failed', {
|
|
@@ -28,6 +28,7 @@ export const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId:
|
|
|
28
28
|
willRenew: null,
|
|
29
29
|
originalTransactionId: null,
|
|
30
30
|
periodType: null,
|
|
31
|
+
packageType: null,
|
|
31
32
|
isPremium: false,
|
|
32
33
|
unsubscribeDetectedAt: null,
|
|
33
34
|
billingIssueDetectedAt: null,
|
|
@@ -41,6 +42,7 @@ export const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId:
|
|
|
41
42
|
willRenew: entitlement.willRenew ?? null,
|
|
42
43
|
originalTransactionId: entitlement.originalPurchaseDate ?? null,
|
|
43
44
|
periodType: validatePeriodType(entitlement.periodType),
|
|
45
|
+
packageType: null,
|
|
44
46
|
isPremium,
|
|
45
47
|
unsubscribeDetectedAt: entitlement.unsubscribeDetectedAt ?? null,
|
|
46
48
|
billingIssueDetectedAt: entitlement.billingIssueDetectedAt ?? null,
|
|
@@ -5,11 +5,11 @@ import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
|
|
|
5
5
|
declare const __DEV__: boolean;
|
|
6
6
|
|
|
7
7
|
export async function startBackgroundInitialization(config: SubscriptionInitConfig): Promise<() => void> {
|
|
8
|
-
const initializeInBackground = async (
|
|
8
|
+
const initializeInBackground = async (revenueCatUserId?: string): Promise<void> => {
|
|
9
9
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
10
|
-
console.log('[BackgroundInitializer] initializeInBackground called with userId:',
|
|
10
|
+
console.log('[BackgroundInitializer] initializeInBackground called with userId:', revenueCatUserId || '(undefined - anonymous)');
|
|
11
11
|
}
|
|
12
|
-
await SubscriptionManager.initialize(
|
|
12
|
+
await SubscriptionManager.initialize(revenueCatUserId);
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const auth = config.getFirebaseAuth();
|
|
@@ -21,22 +21,22 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
|
|
|
21
21
|
console.log('[BackgroundInitializer] Starting background initialization');
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const initialRevenueCatUserId = getCurrentUserId(() => auth);
|
|
25
25
|
|
|
26
26
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
27
|
-
console.log('[BackgroundInitializer] Initial userId:',
|
|
27
|
+
console.log('[BackgroundInitializer] Initial RevenueCat userId:', initialRevenueCatUserId || '(undefined - anonymous)');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
await initializeInBackground(
|
|
30
|
+
await initializeInBackground(initialRevenueCatUserId);
|
|
31
31
|
|
|
32
|
-
const unsubscribe = setupAuthStateListener(() => auth, async (
|
|
32
|
+
const unsubscribe = setupAuthStateListener(() => auth, async (newRevenueCatUserId) => {
|
|
33
33
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
34
|
-
console.log('[BackgroundInitializer] Auth state listener triggered, reinitializing with userId:',
|
|
34
|
+
console.log('[BackgroundInitializer] Auth state listener triggered, reinitializing with userId:', newRevenueCatUserId || '(undefined - anonymous)');
|
|
35
35
|
}
|
|
36
36
|
try {
|
|
37
|
-
await initializeInBackground(
|
|
37
|
+
await initializeInBackground(newRevenueCatUserId);
|
|
38
38
|
} catch (error) {
|
|
39
|
-
console.error('[BackgroundInitializer] Failed to reinitialize on auth change', { userId:
|
|
39
|
+
console.error('[BackgroundInitializer] Failed to reinitialize on auth change', { userId: newRevenueCatUserId, error });
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -5,7 +5,7 @@ import { SubscriptionSyncService } from "../SubscriptionSyncService";
|
|
|
5
5
|
import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
|
|
6
6
|
|
|
7
7
|
export function configureServices(config: SubscriptionInitConfig, apiKey: string): SubscriptionSyncService {
|
|
8
|
-
const { entitlementId, credits, creditPackages, getFirebaseAuth, showAuthModal, onCreditsUpdated } = config;
|
|
8
|
+
const { entitlementId, credits, creditPackages, getFirebaseAuth, showAuthModal, onCreditsUpdated, getAnonymousUserId } = config;
|
|
9
9
|
|
|
10
10
|
if (!creditPackages) {
|
|
11
11
|
throw new Error('[ServiceConfigurator] creditPackages configuration is required');
|
|
@@ -16,14 +16,14 @@ export function configureServices(config: SubscriptionInitConfig, apiKey: string
|
|
|
16
16
|
creditPackageAmounts: creditPackages.amounts
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
const syncService = new SubscriptionSyncService(entitlementId);
|
|
19
|
+
const syncService = new SubscriptionSyncService(entitlementId, getAnonymousUserId);
|
|
20
20
|
|
|
21
21
|
SubscriptionManager.configure({
|
|
22
22
|
config: {
|
|
23
23
|
apiKey,
|
|
24
24
|
entitlementIdentifier: entitlementId,
|
|
25
25
|
consumableProductIdentifiers: [creditPackages.identifierPattern],
|
|
26
|
-
onPurchaseCompleted: (u: string, p: string, c: any, s: any) => syncService.handlePurchase(u, p, c, s),
|
|
26
|
+
onPurchaseCompleted: (u: string, p: string, c: any, s: any, pkgType: any) => syncService.handlePurchase(u, p, c, s, pkgType),
|
|
27
27
|
onRenewalDetected: (u: string, p: string, expires: string, c: any) => syncService.handleRenewal(u, p, expires, c),
|
|
28
28
|
onPremiumStatusChanged: (u: string, isP: boolean, pId: any, exp: any, willR: any, pt: any) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt),
|
|
29
29
|
onCreditsUpdated,
|
|
@@ -12,6 +12,7 @@ export interface PremiumStatus {
|
|
|
12
12
|
billingIssuesDetected: boolean;
|
|
13
13
|
isSandbox: boolean;
|
|
14
14
|
periodType: string | null;
|
|
15
|
+
packageType: string | null;
|
|
15
16
|
store: string | null;
|
|
16
17
|
gracePeriodExpiresDate: Date | null;
|
|
17
18
|
unsubscribeDetectedAt: Date | null;
|
|
@@ -32,6 +33,7 @@ export class PurchaseStatusResolver {
|
|
|
32
33
|
billingIssuesDetected: entitlement.billingIssueDetectedAt !== null && entitlement.billingIssueDetectedAt !== undefined,
|
|
33
34
|
isSandbox: entitlement.isSandbox ?? false,
|
|
34
35
|
periodType: entitlement.periodType ?? null,
|
|
36
|
+
packageType: null,
|
|
35
37
|
store: null,
|
|
36
38
|
gracePeriodExpiresDate: null,
|
|
37
39
|
unsubscribeDetectedAt: toDate(entitlement.unsubscribeDetectedAt) ?? null,
|
|
@@ -48,6 +50,7 @@ export class PurchaseStatusResolver {
|
|
|
48
50
|
billingIssuesDetected: false,
|
|
49
51
|
isSandbox: false,
|
|
50
52
|
periodType: null,
|
|
53
|
+
packageType: null,
|
|
51
54
|
store: null,
|
|
52
55
|
gracePeriodExpiresDate: null,
|
|
53
56
|
unsubscribeDetectedAt: null,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
isNetworkError,
|
|
13
13
|
isInvalidCredentialsError,
|
|
14
14
|
} from "../../../revenuecat/core/types";
|
|
15
|
-
import {
|
|
15
|
+
import { notifyRestoreCompleted } from "../utils/PremiumStatusSyncer";
|
|
16
16
|
|
|
17
17
|
export interface RestoreHandlerDeps {
|
|
18
18
|
config: RevenueCatConfig;
|
|
@@ -28,9 +28,6 @@ export async function handleRestore(deps: RestoreHandlerDeps, userId: string): P
|
|
|
28
28
|
const isPremium = !!entitlement;
|
|
29
29
|
const productId = entitlement?.productIdentifier ?? null;
|
|
30
30
|
|
|
31
|
-
if (isPremium) {
|
|
32
|
-
await syncPremiumStatus(deps.config, userId, customerInfo);
|
|
33
|
-
}
|
|
34
31
|
await notifyRestoreCompleted(deps.config, userId, isPremium, customerInfo);
|
|
35
32
|
|
|
36
33
|
return { success: true, isPremium, productId, customerInfo };
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import Purchases, { type PurchasesPackage, type CustomerInfo } from "react-native-purchases";
|
|
2
2
|
import type { PurchaseResult } from "../../../../../shared/application/ports/IRevenueCatService";
|
|
3
|
-
import type { RevenueCatConfig } from "../../../../revenuecat/core/types";
|
|
4
|
-
import {
|
|
3
|
+
import type { RevenueCatConfig, PackageType } from "../../../../revenuecat/core/types";
|
|
4
|
+
import { notifyPurchaseCompleted } from "../../utils/PremiumStatusSyncer";
|
|
5
5
|
import { getSavedPurchase, clearSavedPurchase } from "../../../presentation/useAuthAwarePurchase";
|
|
6
6
|
|
|
7
7
|
async function executeConsumablePurchase(
|
|
8
8
|
config: RevenueCatConfig,
|
|
9
9
|
userId: string,
|
|
10
10
|
productId: string,
|
|
11
|
-
customerInfo: CustomerInfo
|
|
11
|
+
customerInfo: CustomerInfo,
|
|
12
|
+
packageType: PackageType | null
|
|
12
13
|
): Promise<PurchaseResult> {
|
|
13
14
|
const savedPurchase = getSavedPurchase();
|
|
14
15
|
const source = savedPurchase?.source;
|
|
@@ -16,7 +17,7 @@ async function executeConsumablePurchase(
|
|
|
16
17
|
clearSavedPurchase();
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
await notifyPurchaseCompleted(config, userId, productId, customerInfo, source);
|
|
20
|
+
await notifyPurchaseCompleted(config, userId, productId, customerInfo, source, packageType);
|
|
20
21
|
|
|
21
22
|
return {
|
|
22
23
|
success: true,
|
|
@@ -34,7 +35,8 @@ async function executeSubscriptionPurchase(
|
|
|
34
35
|
userId: string,
|
|
35
36
|
productId: string,
|
|
36
37
|
customerInfo: CustomerInfo,
|
|
37
|
-
entitlementIdentifier: string
|
|
38
|
+
entitlementIdentifier: string,
|
|
39
|
+
packageType: PackageType | null
|
|
38
40
|
): Promise<PurchaseResult> {
|
|
39
41
|
const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
|
|
40
42
|
const savedPurchase = getSavedPurchase();
|
|
@@ -51,16 +53,11 @@ async function executeSubscriptionPurchase(
|
|
|
51
53
|
entitlementIdentifier,
|
|
52
54
|
activeEntitlements: Object.keys(customerInfo.entitlements.active),
|
|
53
55
|
source,
|
|
56
|
+
packageType,
|
|
54
57
|
});
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
await
|
|
58
|
-
|
|
59
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
60
|
-
console.log("[PurchaseExecutor] syncPremiumStatus completed");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
await notifyPurchaseCompleted(config, userId, productId, customerInfo, source);
|
|
60
|
+
await notifyPurchaseCompleted(config, userId, productId, customerInfo, source, packageType);
|
|
64
61
|
|
|
65
62
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
66
63
|
console.log("[PurchaseExecutor] Purchase flow completed successfully");
|
|
@@ -83,7 +80,8 @@ export async function executePurchase(
|
|
|
83
80
|
console.log('🔵 [PurchaseExecutor] executePurchase called', {
|
|
84
81
|
productId: pkg.product.identifier,
|
|
85
82
|
userId,
|
|
86
|
-
isConsumable
|
|
83
|
+
isConsumable,
|
|
84
|
+
packageType: pkg.packageType
|
|
87
85
|
});
|
|
88
86
|
|
|
89
87
|
console.log('🚀 [PurchaseExecutor] Calling Purchases.purchasePackage (RevenueCat SDK)');
|
|
@@ -91,10 +89,11 @@ export async function executePurchase(
|
|
|
91
89
|
console.log('✅ [PurchaseExecutor] Purchases.purchasePackage completed');
|
|
92
90
|
|
|
93
91
|
const productId = pkg.product.identifier;
|
|
92
|
+
const packageType = pkg.packageType ?? null;
|
|
94
93
|
|
|
95
94
|
if (isConsumable) {
|
|
96
95
|
console.log('💰 [PurchaseExecutor] Processing as consumable purchase');
|
|
97
|
-
return executeConsumablePurchase(config, userId, productId, customerInfo);
|
|
96
|
+
return executeConsumablePurchase(config, userId, productId, customerInfo, packageType);
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
console.log('📅 [PurchaseExecutor] Processing as subscription purchase');
|
|
@@ -103,6 +102,7 @@ export async function executePurchase(
|
|
|
103
102
|
userId,
|
|
104
103
|
productId,
|
|
105
104
|
customerInfo,
|
|
106
|
-
config.entitlementIdentifier
|
|
105
|
+
config.entitlementIdentifier,
|
|
106
|
+
packageType
|
|
107
107
|
);
|
|
108
108
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { CustomerInfo } from "react-native-purchases";
|
|
7
|
-
import type { RevenueCatConfig } from "../../../revenuecat/core/types";
|
|
7
|
+
import type { RevenueCatConfig, PackageType } from "../../../revenuecat/core/types";
|
|
8
8
|
import type { PurchaseSource } from "../../../subscription/core/SubscriptionConstants";
|
|
9
9
|
import { getPremiumEntitlement } from "../../../revenuecat/core/types";
|
|
10
10
|
|
|
@@ -88,19 +88,21 @@ export async function notifyPurchaseCompleted(
|
|
|
88
88
|
userId: string,
|
|
89
89
|
productId: string,
|
|
90
90
|
customerInfo: CustomerInfo,
|
|
91
|
-
source?: PurchaseSource
|
|
91
|
+
source?: PurchaseSource,
|
|
92
|
+
packageType?: PackageType | null
|
|
92
93
|
): Promise<void> {
|
|
93
94
|
if (!config.onPurchaseCompleted) {
|
|
94
95
|
return;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
try {
|
|
98
|
-
await config.onPurchaseCompleted(userId, productId, customerInfo, source);
|
|
99
|
+
await config.onPurchaseCompleted(userId, productId, customerInfo, source, packageType);
|
|
99
100
|
} catch (error) {
|
|
100
101
|
console.error('[PremiumStatusSyncer] Purchase completion callback failed', {
|
|
101
102
|
userId,
|
|
102
103
|
productId,
|
|
103
104
|
source,
|
|
105
|
+
packageType,
|
|
104
106
|
error
|
|
105
107
|
});
|
|
106
108
|
// Silently fail callback notifications to prevent crashing the main flow
|
|
@@ -55,6 +55,7 @@ export const SubscriptionDetailScreen: React.FC<SubscriptionDetailScreenProps> =
|
|
|
55
55
|
productIdentifier={config.productIdentifier}
|
|
56
56
|
productName={config.productName}
|
|
57
57
|
periodType={config.periodType}
|
|
58
|
+
packageType={config.packageType}
|
|
58
59
|
store={config.store}
|
|
59
60
|
originalPurchaseDate={config.originalPurchaseDate}
|
|
60
61
|
latestPurchaseDate={config.latestPurchaseDate}
|
|
@@ -62,6 +62,7 @@ export interface SubscriptionDetailConfig {
|
|
|
62
62
|
productIdentifier?: string | null;
|
|
63
63
|
productName?: string | null;
|
|
64
64
|
periodType?: string | null;
|
|
65
|
+
packageType?: string | null;
|
|
65
66
|
store?: string | null;
|
|
66
67
|
originalPurchaseDate?: string | null;
|
|
67
68
|
latestPurchaseDate?: string | null;
|
|
@@ -20,6 +20,7 @@ export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
|
|
|
20
20
|
translations,
|
|
21
21
|
willRenew,
|
|
22
22
|
periodType,
|
|
23
|
+
packageType,
|
|
23
24
|
store,
|
|
24
25
|
originalPurchaseDate,
|
|
25
26
|
latestPurchaseDate,
|
|
@@ -59,6 +60,7 @@ export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
|
|
|
59
60
|
styles={styles}
|
|
60
61
|
willRenew={willRenew}
|
|
61
62
|
periodType={periodType}
|
|
63
|
+
packageType={packageType}
|
|
62
64
|
store={store}
|
|
63
65
|
originalPurchaseDate={originalPurchaseDate}
|
|
64
66
|
latestPurchaseDate={latestPurchaseDate}
|
package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.types.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface SubscriptionHeaderProps {
|
|
|
31
31
|
productIdentifier?: string | null;
|
|
32
32
|
productName?: string | null;
|
|
33
33
|
periodType?: string | null;
|
|
34
|
+
packageType?: string | null;
|
|
34
35
|
store?: string | null;
|
|
35
36
|
originalPurchaseDate?: string | null;
|
|
36
37
|
latestPurchaseDate?: string | null;
|
package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx
CHANGED
|
@@ -3,6 +3,21 @@ import { View } from "react-native";
|
|
|
3
3
|
import { DetailRow } from "../../components/details/DetailRow";
|
|
4
4
|
import type { SubscriptionHeaderProps } from "./SubscriptionHeader.types";
|
|
5
5
|
|
|
6
|
+
declare const __DEV__: boolean;
|
|
7
|
+
|
|
8
|
+
function formatSubscriptionPeriod(packageType: string | null | undefined, periodType: string | null | undefined): string {
|
|
9
|
+
if (packageType) {
|
|
10
|
+
const formatted = packageType.toLowerCase().replace(/_/g, ' ');
|
|
11
|
+
return formatted.charAt(0).toUpperCase() + formatted.slice(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (periodType === "NORMAL") {
|
|
15
|
+
return "Standard";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return periodType || "Unknown";
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
interface SubscriptionHeaderContentProps {
|
|
7
22
|
isLifetime: boolean;
|
|
8
23
|
showExpirationDate: boolean;
|
|
@@ -13,6 +28,7 @@ interface SubscriptionHeaderContentProps {
|
|
|
13
28
|
styles: any;
|
|
14
29
|
willRenew?: boolean | null;
|
|
15
30
|
periodType?: string | null;
|
|
31
|
+
packageType?: string | null;
|
|
16
32
|
store?: string | null;
|
|
17
33
|
originalPurchaseDate?: string | null;
|
|
18
34
|
latestPurchaseDate?: string | null;
|
|
@@ -30,6 +46,7 @@ export const SubscriptionHeaderContent: React.FC<SubscriptionHeaderContentProps>
|
|
|
30
46
|
styles,
|
|
31
47
|
willRenew,
|
|
32
48
|
periodType,
|
|
49
|
+
packageType,
|
|
33
50
|
store,
|
|
34
51
|
originalPurchaseDate,
|
|
35
52
|
latestPurchaseDate,
|
|
@@ -76,10 +93,10 @@ export const SubscriptionHeaderContent: React.FC<SubscriptionHeaderContentProps>
|
|
|
76
93
|
valueStyle={styles.value}
|
|
77
94
|
/>
|
|
78
95
|
)}
|
|
79
|
-
{periodType && translations.periodTypeLabel && (
|
|
96
|
+
{(packageType || periodType) && translations.periodTypeLabel && (
|
|
80
97
|
<DetailRow
|
|
81
98
|
label={translations.periodTypeLabel}
|
|
82
|
-
value={
|
|
99
|
+
value={formatSubscriptionPeriod(packageType, periodType)}
|
|
83
100
|
style={styles.row}
|
|
84
101
|
labelStyle={styles.label}
|
|
85
102
|
valueStyle={styles.value}
|
|
@@ -122,7 +139,7 @@ export const SubscriptionHeaderContent: React.FC<SubscriptionHeaderContentProps>
|
|
|
122
139
|
valueStyle={styles.value}
|
|
123
140
|
/>
|
|
124
141
|
)}
|
|
125
|
-
{isSandbox && translations.sandboxLabel && (
|
|
142
|
+
{typeof __DEV__ !== 'undefined' && __DEV__ && isSandbox && translations.sandboxLabel && (
|
|
126
143
|
<DetailRow
|
|
127
144
|
label={translations.sandboxLabel}
|
|
128
145
|
value="Test Mode"
|
|
@@ -89,6 +89,7 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
|
89
89
|
billingIssuesDetected: data?.billingIssuesDetected ?? false,
|
|
90
90
|
isSandbox: data?.isSandbox ?? false,
|
|
91
91
|
periodType: data?.periodType ?? null,
|
|
92
|
+
packageType: data?.packageType ?? null,
|
|
92
93
|
store: data?.store ?? null,
|
|
93
94
|
gracePeriodExpiresDate: data?.gracePeriodExpiresDate ?? null,
|
|
94
95
|
unsubscribeDetectedAt: data?.unsubscribeDetectedAt ?? null,
|