@umituz/react-native-subscription 2.31.5 → 2.31.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/credits/core/Credits.ts +2 -3
- package/src/domains/credits/core/CreditsMapper.ts +2 -3
- package/src/domains/credits/core/UserCreditsDocument.ts +2 -4
- package/src/domains/credits/infrastructure/CreditsRepository.ts +1 -1
- package/src/domains/revenuecat/core/constants/RevenueCatConstants.ts +201 -0
- package/src/domains/revenuecat/core/constants/index.ts +5 -0
- package/src/domains/{subscription/core → revenuecat/core/errors}/RevenueCatError.ts +1 -1
- package/src/domains/revenuecat/core/errors/RevenueCatErrorHandler.ts +91 -0
- package/src/domains/revenuecat/core/errors/RevenueCatErrorMessages.ts +140 -0
- package/src/domains/revenuecat/core/errors/index.ts +7 -0
- package/src/domains/revenuecat/core/index.ts +7 -0
- package/src/domains/{subscription/core → revenuecat/core/types}/RevenueCatConfig.ts +6 -3
- package/src/domains/{subscription/core → revenuecat/core/types}/RevenueCatData.ts +4 -4
- package/src/domains/{subscription/core → revenuecat/core/types}/RevenueCatTypes.ts +11 -6
- package/src/domains/revenuecat/core/types/index.ts +7 -0
- package/src/domains/revenuecat/index.ts +7 -0
- package/src/domains/revenuecat/infrastructure/index.ts +5 -0
- package/src/domains/{subscription → revenuecat}/infrastructure/services/RevenueCatInitializer.ts +16 -5
- package/src/domains/{subscription → revenuecat}/infrastructure/services/RevenueCatInitializer.types.ts +1 -1
- package/src/domains/revenuecat/infrastructure/services/index.ts +7 -0
- package/src/domains/{subscription → revenuecat}/infrastructure/utils/ApiKeyResolver.ts +1 -1
- package/src/domains/subscription/application/SubscriptionAuthListener.ts +13 -2
- package/src/domains/subscription/application/SubscriptionInitializerTypes.ts +2 -3
- package/src/domains/subscription/application/SubscriptionSyncUtils.ts +1 -1
- package/src/domains/subscription/application/statusChangeHandlers.ts +1 -1
- package/src/domains/subscription/application/syncConstants.ts +1 -1
- package/src/domains/subscription/core/SubscriptionStatus.ts +2 -2
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +1 -1
- package/src/domains/subscription/infrastructure/handlers/PurchaseStatusResolver.ts +1 -1
- package/src/domains/subscription/infrastructure/hooks/usePurchasePackage.ts +14 -54
- package/src/domains/subscription/infrastructure/hooks/useRestorePurchase.ts +10 -40
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.types.ts +1 -1
- package/src/domains/subscription/infrastructure/managers/initializationHandler.ts +1 -1
- package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +1 -1
- package/src/domains/subscription/infrastructure/services/PurchaseHandler.ts +5 -5
- package/src/domains/subscription/infrastructure/services/RestoreHandler.ts +5 -5
- package/src/domains/subscription/infrastructure/services/RevenueCatService.types.ts +3 -3
- package/src/domains/subscription/infrastructure/services/ServiceStateManager.ts +1 -1
- package/src/domains/subscription/infrastructure/services/revenueCatServiceInstance.ts +1 -1
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +2 -2
- package/src/domains/subscription/core/RevenueCatConstants.ts +0 -73
- /package/src/domains/{subscription → revenuecat}/infrastructure/services/initializerConstants.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.31.
|
|
3
|
+
"version": "2.31.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",
|
|
@@ -9,7 +9,6 @@ import type { SubscriptionPackageType } from "../../../utils/packageTypeDetector
|
|
|
9
9
|
// Types imported from SubscriptionConstants are used directly in UserCredits interface
|
|
10
10
|
import type {
|
|
11
11
|
SubscriptionStatusType,
|
|
12
|
-
PeriodType,
|
|
13
12
|
PackageType,
|
|
14
13
|
Platform,
|
|
15
14
|
PurchaseSource,
|
|
@@ -36,8 +35,8 @@ export interface UserCredits {
|
|
|
36
35
|
packageType: PackageType | null;
|
|
37
36
|
originalTransactionId: string | null;
|
|
38
37
|
|
|
39
|
-
// Trial fields
|
|
40
|
-
periodType:
|
|
38
|
+
// Trial fields - periodType comes from RevenueCat SDK
|
|
39
|
+
periodType: string | null;
|
|
41
40
|
isTrialing: boolean | null;
|
|
42
41
|
trialStartDate: Date | null;
|
|
43
42
|
trialEndDate: Date | null;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { UserCredits } from "./Credits";
|
|
2
|
-
import { resolveSubscriptionStatus } from "../../subscription/core/SubscriptionStatus";
|
|
3
|
-
import type { PeriodType, SubscriptionStatusType } from "../../subscription/core/SubscriptionConstants";
|
|
2
|
+
import { resolveSubscriptionStatus, type SubscriptionStatusType } from "../../subscription/core/SubscriptionStatus";
|
|
4
3
|
import type { UserCreditsDocumentRead } from "./UserCreditsDocument";
|
|
5
4
|
import { toSafeDate } from "../../../utils/dateUtils";
|
|
6
5
|
|
|
@@ -10,7 +9,7 @@ import { toSafeDate } from "../../../utils/dateUtils";
|
|
|
10
9
|
function validateSubscription(
|
|
11
10
|
doc: UserCreditsDocumentRead,
|
|
12
11
|
expirationDate: Date | null,
|
|
13
|
-
periodType:
|
|
12
|
+
periodType: string | null
|
|
14
13
|
): { isPremium: boolean; status: SubscriptionStatusType } {
|
|
15
14
|
const isPremium = doc.isPremium;
|
|
16
15
|
const willRenew = doc.willRenew ?? false;
|
|
@@ -2,17 +2,15 @@ import type {
|
|
|
2
2
|
PurchaseSource,
|
|
3
3
|
PurchaseType,
|
|
4
4
|
SubscriptionStatusType,
|
|
5
|
-
PeriodType,
|
|
6
5
|
PackageType,
|
|
7
6
|
Platform
|
|
8
7
|
} from "../../subscription/core/SubscriptionConstants";
|
|
9
|
-
import type { Store, OwnershipType } from "../../
|
|
8
|
+
import type { Store, OwnershipType } from "../../revenuecat/core/types";
|
|
10
9
|
|
|
11
10
|
export type {
|
|
12
11
|
PurchaseSource,
|
|
13
12
|
PurchaseType,
|
|
14
13
|
SubscriptionStatusType,
|
|
15
|
-
PeriodType,
|
|
16
14
|
Store,
|
|
17
15
|
OwnershipType
|
|
18
16
|
};
|
|
@@ -55,7 +53,7 @@ export interface UserCreditsDocumentRead {
|
|
|
55
53
|
ownershipType: OwnershipType | null;
|
|
56
54
|
|
|
57
55
|
// Trial fields
|
|
58
|
-
periodType:
|
|
56
|
+
periodType: string | null;
|
|
59
57
|
isTrialing: boolean | null;
|
|
60
58
|
trialStartDate: FirestoreTimestamp | null;
|
|
61
59
|
trialEndDate: FirestoreTimestamp | null;
|
|
@@ -4,7 +4,7 @@ import type { CreditsConfig, CreditsResult, DeductCreditsResult } from "../core/
|
|
|
4
4
|
import type { UserCreditsDocumentRead, PurchaseSource } from "../core/UserCreditsDocument";
|
|
5
5
|
import { initializeCreditsTransaction } from "../application/CreditsInitializer";
|
|
6
6
|
import { mapCreditsDocumentToEntity } from "../core/CreditsMapper";
|
|
7
|
-
import type { RevenueCatData } from "../../
|
|
7
|
+
import type { RevenueCatData } from "../../revenuecat/core/types";
|
|
8
8
|
import { deductCreditsOperation } from "../application/DeductCreditsCommand";
|
|
9
9
|
import { calculateCreditLimit } from "../application/CreditLimitCalculator";
|
|
10
10
|
import { PURCHASE_TYPE, type PurchaseType } from "../../subscription/core/SubscriptionConstants";
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RevenueCat Constants
|
|
3
|
+
* Error codes, messages, and logging constants
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Purchases from "react-native-purchases";
|
|
7
|
+
|
|
8
|
+
export const REVENUECAT_LOG_PREFIX = "[RevenueCat]";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* RevenueCat Error Code Type
|
|
12
|
+
* Re-export for type safety
|
|
13
|
+
*/
|
|
14
|
+
export type PurchasesErrorCode = typeof Purchases.PURCHASES_ERROR_CODE[keyof typeof Purchases.PURCHASES_ERROR_CODE];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Error Message Configuration
|
|
18
|
+
*/
|
|
19
|
+
export interface ErrorMessage {
|
|
20
|
+
title: string;
|
|
21
|
+
message: string;
|
|
22
|
+
shouldShowAlert?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Error Code to Enum Mapping
|
|
27
|
+
* Maps both string keys and numeric codes to Purchases error enum values
|
|
28
|
+
*/
|
|
29
|
+
const ERROR_CODE_MAP = new Map<string, PurchasesErrorCode>([
|
|
30
|
+
["PURCHASE_CANCELLED_ERROR", Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR],
|
|
31
|
+
["PURCHASE_NOT_ALLOWED_ERROR", Purchases.PURCHASES_ERROR_CODE.PURCHASE_NOT_ALLOWED_ERROR],
|
|
32
|
+
["PURCHASE_INVALID_ERROR", Purchases.PURCHASES_ERROR_CODE.PURCHASE_INVALID_ERROR],
|
|
33
|
+
["PRODUCT_ALREADY_PURCHASED_ERROR", Purchases.PURCHASES_ERROR_CODE.PRODUCT_ALREADY_PURCHASED_ERROR],
|
|
34
|
+
["PRODUCT_NOT_AVAILABLE_FOR_PURCHASE_ERROR", Purchases.PURCHASES_ERROR_CODE.PRODUCT_NOT_AVAILABLE_FOR_PURCHASE_ERROR],
|
|
35
|
+
["NETWORK_ERROR", Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR],
|
|
36
|
+
["UNKNOWN_ERROR", Purchases.PURCHASES_ERROR_CODE.UNKNOWN_ERROR],
|
|
37
|
+
["RECEIPT_ALREADY_IN_USE_ERROR", Purchases.PURCHASES_ERROR_CODE.RECEIPT_ALREADY_IN_USE_ERROR],
|
|
38
|
+
["INVALID_CREDENTIALS_ERROR", Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR],
|
|
39
|
+
["UNEXPECTED_BACKEND_RESPONSE_ERROR", Purchases.PURCHASES_ERROR_CODE.UNEXPECTED_BACKEND_RESPONSE_ERROR],
|
|
40
|
+
["CONFIGURATION_ERROR", Purchases.PURCHASES_ERROR_CODE.CONFIGURATION_ERROR],
|
|
41
|
+
["STORE_PROBLEM_ERROR", Purchases.PURCHASES_ERROR_CODE.STORE_PROBLEM_ERROR],
|
|
42
|
+
["PAYMENT_PENDING_ERROR", Purchases.PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR],
|
|
43
|
+
// Numeric codes as fallback
|
|
44
|
+
["1", Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR],
|
|
45
|
+
["2", Purchases.PURCHASES_ERROR_CODE.STORE_PROBLEM_ERROR],
|
|
46
|
+
["3", Purchases.PURCHASES_ERROR_CODE.PURCHASE_NOT_ALLOWED_ERROR],
|
|
47
|
+
["4", Purchases.PURCHASES_ERROR_CODE.PURCHASE_INVALID_ERROR],
|
|
48
|
+
["5", Purchases.PURCHASES_ERROR_CODE.PRODUCT_NOT_AVAILABLE_FOR_PURCHASE_ERROR],
|
|
49
|
+
["6", Purchases.PURCHASES_ERROR_CODE.PRODUCT_ALREADY_PURCHASED_ERROR],
|
|
50
|
+
["7", Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR],
|
|
51
|
+
["8", Purchases.PURCHASES_ERROR_CODE.RECEIPT_ALREADY_IN_USE_ERROR],
|
|
52
|
+
["9", Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR],
|
|
53
|
+
["10", Purchases.PURCHASES_ERROR_CODE.UNEXPECTED_BACKEND_RESPONSE_ERROR],
|
|
54
|
+
["16", Purchases.PURCHASES_ERROR_CODE.CONFIGURATION_ERROR],
|
|
55
|
+
["20", Purchases.PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR],
|
|
56
|
+
["0", Purchases.PURCHASES_ERROR_CODE.UNKNOWN_ERROR],
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* User-friendly error messages mapped by error code enum
|
|
61
|
+
* Strategy Pattern: Each error code has its own message configuration
|
|
62
|
+
*/
|
|
63
|
+
const ERROR_MESSAGES_MAP = new Map<PurchasesErrorCode, ErrorMessage>([
|
|
64
|
+
[
|
|
65
|
+
Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR,
|
|
66
|
+
{
|
|
67
|
+
title: "Purchase Cancelled",
|
|
68
|
+
message: "The purchase was cancelled.",
|
|
69
|
+
shouldShowAlert: false, // Don't show alert for user cancellation
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
Purchases.PURCHASES_ERROR_CODE.PURCHASE_NOT_ALLOWED_ERROR,
|
|
74
|
+
{
|
|
75
|
+
title: "Purchase Not Allowed",
|
|
76
|
+
message: "In-app purchases are disabled on this device.",
|
|
77
|
+
shouldShowAlert: true,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
Purchases.PURCHASES_ERROR_CODE.PURCHASE_INVALID_ERROR,
|
|
82
|
+
{
|
|
83
|
+
title: "Invalid Purchase",
|
|
84
|
+
message: "The purchase is invalid. Please contact support.",
|
|
85
|
+
shouldShowAlert: true,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
Purchases.PURCHASES_ERROR_CODE.PRODUCT_ALREADY_PURCHASED_ERROR,
|
|
90
|
+
{
|
|
91
|
+
title: "Already Purchased",
|
|
92
|
+
message: "You already own this subscription. Restoring your purchase...",
|
|
93
|
+
shouldShowAlert: true,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
Purchases.PURCHASES_ERROR_CODE.PRODUCT_NOT_AVAILABLE_FOR_PURCHASE_ERROR,
|
|
98
|
+
{
|
|
99
|
+
title: "Product Unavailable",
|
|
100
|
+
message: "This product is not available for purchase at this time.",
|
|
101
|
+
shouldShowAlert: true,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR,
|
|
106
|
+
{
|
|
107
|
+
title: "Network Error",
|
|
108
|
+
message: "Please check your internet connection and try again.",
|
|
109
|
+
shouldShowAlert: true,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
Purchases.PURCHASES_ERROR_CODE.UNKNOWN_ERROR,
|
|
114
|
+
{
|
|
115
|
+
title: "Unknown Error",
|
|
116
|
+
message: "An unexpected error occurred. Please try again.",
|
|
117
|
+
shouldShowAlert: true,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
[
|
|
121
|
+
Purchases.PURCHASES_ERROR_CODE.RECEIPT_ALREADY_IN_USE_ERROR,
|
|
122
|
+
{
|
|
123
|
+
title: "Receipt Already Used",
|
|
124
|
+
message: "This receipt is already associated with another account.",
|
|
125
|
+
shouldShowAlert: true,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
[
|
|
129
|
+
Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR,
|
|
130
|
+
{
|
|
131
|
+
title: "Configuration Error",
|
|
132
|
+
message: "The app is not configured correctly. Please contact support.",
|
|
133
|
+
shouldShowAlert: true,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
Purchases.PURCHASES_ERROR_CODE.UNEXPECTED_BACKEND_RESPONSE_ERROR,
|
|
138
|
+
{
|
|
139
|
+
title: "Server Error",
|
|
140
|
+
message: "The server returned an unexpected response. Please try again later.",
|
|
141
|
+
shouldShowAlert: true,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
Purchases.PURCHASES_ERROR_CODE.CONFIGURATION_ERROR,
|
|
146
|
+
{
|
|
147
|
+
title: "Configuration Error",
|
|
148
|
+
message: "RevenueCat is not configured correctly. Please contact support.",
|
|
149
|
+
shouldShowAlert: true,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
[
|
|
153
|
+
Purchases.PURCHASES_ERROR_CODE.STORE_PROBLEM_ERROR,
|
|
154
|
+
{
|
|
155
|
+
title: "Store Error",
|
|
156
|
+
message: "There was a problem with the app store. Please try again.",
|
|
157
|
+
shouldShowAlert: true,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
[
|
|
161
|
+
Purchases.PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR,
|
|
162
|
+
{
|
|
163
|
+
title: "Payment Pending",
|
|
164
|
+
message: "Your payment is still being processed. Please check back later.",
|
|
165
|
+
shouldShowAlert: true,
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Default error message for unknown errors
|
|
172
|
+
*/
|
|
173
|
+
const DEFAULT_ERROR_MESSAGE: ErrorMessage = {
|
|
174
|
+
title: "Error",
|
|
175
|
+
message: "An error occurred. Please try again.",
|
|
176
|
+
shouldShowAlert: true,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get error message configuration for a given error code
|
|
181
|
+
* Uses Strategy Pattern with Map lookup - O(1) complexity
|
|
182
|
+
*
|
|
183
|
+
* @param errorCode - Error code string from RevenueCat error
|
|
184
|
+
* @returns ErrorMessage configuration
|
|
185
|
+
*/
|
|
186
|
+
export function getErrorMessageForCode(errorCode: string | null | undefined): ErrorMessage {
|
|
187
|
+
if (!errorCode) {
|
|
188
|
+
return DEFAULT_ERROR_MESSAGE;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Try to map string code to enum value
|
|
192
|
+
const enumCode = ERROR_CODE_MAP.get(errorCode);
|
|
193
|
+
if (enumCode) {
|
|
194
|
+
const message = ERROR_MESSAGES_MAP.get(enumCode);
|
|
195
|
+
if (message) {
|
|
196
|
+
return message;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return DEFAULT_ERROR_MESSAGE;
|
|
201
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Domain-specific error types for RevenueCat operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { BaseError } from "
|
|
6
|
+
import { BaseError } from "../../../../shared/utils/BaseError";
|
|
7
7
|
|
|
8
8
|
export class RevenueCatError extends BaseError {
|
|
9
9
|
constructor(message: string, code: string = 'REVENUE_CAT_ERROR', cause?: Error) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RevenueCat Error Handler
|
|
3
|
+
* Error code mapping and message resolution utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Purchases from "react-native-purchases";
|
|
7
|
+
import {
|
|
8
|
+
ERROR_MESSAGES_MAP,
|
|
9
|
+
DEFAULT_ERROR_MESSAGE,
|
|
10
|
+
type ErrorMessage,
|
|
11
|
+
type PurchasesErrorCode,
|
|
12
|
+
} from "./RevenueCatErrorMessages";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Error Code to Enum Mapping
|
|
16
|
+
* Maps both string keys and numeric codes to Purchases error enum values
|
|
17
|
+
*/
|
|
18
|
+
const ERROR_CODE_MAP = new Map<string, PurchasesErrorCode>([
|
|
19
|
+
// String error codes
|
|
20
|
+
["PURCHASE_CANCELLED_ERROR", Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR],
|
|
21
|
+
["PURCHASE_NOT_ALLOWED_ERROR", Purchases.PURCHASES_ERROR_CODE.PURCHASE_NOT_ALLOWED_ERROR],
|
|
22
|
+
["PURCHASE_INVALID_ERROR", Purchases.PURCHASES_ERROR_CODE.PURCHASE_INVALID_ERROR],
|
|
23
|
+
["PRODUCT_ALREADY_PURCHASED_ERROR", Purchases.PURCHASES_ERROR_CODE.PRODUCT_ALREADY_PURCHASED_ERROR],
|
|
24
|
+
["PRODUCT_NOT_AVAILABLE_FOR_PURCHASE_ERROR", Purchases.PURCHASES_ERROR_CODE.PRODUCT_NOT_AVAILABLE_FOR_PURCHASE_ERROR],
|
|
25
|
+
["NETWORK_ERROR", Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR],
|
|
26
|
+
["UNKNOWN_ERROR", Purchases.PURCHASES_ERROR_CODE.UNKNOWN_ERROR],
|
|
27
|
+
["RECEIPT_ALREADY_IN_USE_ERROR", Purchases.PURCHASES_ERROR_CODE.RECEIPT_ALREADY_IN_USE_ERROR],
|
|
28
|
+
["INVALID_CREDENTIALS_ERROR", Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR],
|
|
29
|
+
["UNEXPECTED_BACKEND_RESPONSE_ERROR", Purchases.PURCHASES_ERROR_CODE.UNEXPECTED_BACKEND_RESPONSE_ERROR],
|
|
30
|
+
["CONFIGURATION_ERROR", Purchases.PURCHASES_ERROR_CODE.CONFIGURATION_ERROR],
|
|
31
|
+
["STORE_PROBLEM_ERROR", Purchases.PURCHASES_ERROR_CODE.STORE_PROBLEM_ERROR],
|
|
32
|
+
["PAYMENT_PENDING_ERROR", Purchases.PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR],
|
|
33
|
+
|
|
34
|
+
// Numeric error codes as fallback
|
|
35
|
+
["1", Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR],
|
|
36
|
+
["2", Purchases.PURCHASES_ERROR_CODE.STORE_PROBLEM_ERROR],
|
|
37
|
+
["3", Purchases.PURCHASES_ERROR_CODE.PURCHASE_NOT_ALLOWED_ERROR],
|
|
38
|
+
["4", Purchases.PURCHASES_ERROR_CODE.PURCHASE_INVALID_ERROR],
|
|
39
|
+
["5", Purchases.PURCHASES_ERROR_CODE.PRODUCT_NOT_AVAILABLE_FOR_PURCHASE_ERROR],
|
|
40
|
+
["6", Purchases.PURCHASES_ERROR_CODE.PRODUCT_ALREADY_PURCHASED_ERROR],
|
|
41
|
+
["7", Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR],
|
|
42
|
+
["8", Purchases.PURCHASES_ERROR_CODE.RECEIPT_ALREADY_IN_USE_ERROR],
|
|
43
|
+
["9", Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR],
|
|
44
|
+
["10", Purchases.PURCHASES_ERROR_CODE.UNEXPECTED_BACKEND_RESPONSE_ERROR],
|
|
45
|
+
["16", Purchases.PURCHASES_ERROR_CODE.CONFIGURATION_ERROR],
|
|
46
|
+
["20", Purchases.PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR],
|
|
47
|
+
["0", Purchases.PURCHASES_ERROR_CODE.UNKNOWN_ERROR],
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get error message configuration for a given error code
|
|
52
|
+
* Strategy Pattern with Map lookup - O(1) complexity
|
|
53
|
+
*
|
|
54
|
+
* @param errorCode - Error code string from RevenueCat error
|
|
55
|
+
* @returns ErrorMessage configuration
|
|
56
|
+
*/
|
|
57
|
+
export function getErrorMessageForCode(errorCode: string | null | undefined): ErrorMessage {
|
|
58
|
+
if (!errorCode) {
|
|
59
|
+
return DEFAULT_ERROR_MESSAGE;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const enumCode = ERROR_CODE_MAP.get(errorCode);
|
|
63
|
+
if (enumCode) {
|
|
64
|
+
const message = ERROR_MESSAGES_MAP.get(enumCode);
|
|
65
|
+
if (message) {
|
|
66
|
+
return message;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return DEFAULT_ERROR_MESSAGE;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get error message from RevenueCat error object
|
|
75
|
+
*
|
|
76
|
+
* @param error - RevenueCat error object
|
|
77
|
+
* @returns ErrorMessage configuration
|
|
78
|
+
*/
|
|
79
|
+
export function getErrorMessage(error: unknown): ErrorMessage {
|
|
80
|
+
if (!error || typeof error !== "object") {
|
|
81
|
+
return DEFAULT_ERROR_MESSAGE;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const errorCode = "code" in error
|
|
85
|
+
? String(error.code)
|
|
86
|
+
: "readableErrorCode" in error
|
|
87
|
+
? String(error.readableErrorCode)
|
|
88
|
+
: null;
|
|
89
|
+
|
|
90
|
+
return getErrorMessageForCode(errorCode);
|
|
91
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RevenueCat Error Messages
|
|
3
|
+
* User-friendly error message configurations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Purchases from "react-native-purchases";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Error Message Configuration
|
|
10
|
+
*/
|
|
11
|
+
export interface ErrorMessage {
|
|
12
|
+
title: string;
|
|
13
|
+
message: string;
|
|
14
|
+
shouldShowAlert?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* RevenueCat Error Code Type
|
|
19
|
+
*/
|
|
20
|
+
export type PurchasesErrorCode = typeof Purchases.PURCHASES_ERROR_CODE[keyof typeof Purchases.PURCHASES_ERROR_CODE];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* User-friendly error messages mapped by error code enum
|
|
24
|
+
* Strategy Pattern: Each error code has its own message configuration
|
|
25
|
+
*/
|
|
26
|
+
export const ERROR_MESSAGES_MAP = new Map<PurchasesErrorCode, ErrorMessage>([
|
|
27
|
+
[
|
|
28
|
+
Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR,
|
|
29
|
+
{
|
|
30
|
+
title: "Purchase Cancelled",
|
|
31
|
+
message: "The purchase was cancelled.",
|
|
32
|
+
shouldShowAlert: false,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
Purchases.PURCHASES_ERROR_CODE.PURCHASE_NOT_ALLOWED_ERROR,
|
|
37
|
+
{
|
|
38
|
+
title: "Purchase Not Allowed",
|
|
39
|
+
message: "In-app purchases are disabled on this device.",
|
|
40
|
+
shouldShowAlert: true,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
Purchases.PURCHASES_ERROR_CODE.PURCHASE_INVALID_ERROR,
|
|
45
|
+
{
|
|
46
|
+
title: "Invalid Purchase",
|
|
47
|
+
message: "The purchase is invalid. Please contact support.",
|
|
48
|
+
shouldShowAlert: true,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
Purchases.PURCHASES_ERROR_CODE.PRODUCT_ALREADY_PURCHASED_ERROR,
|
|
53
|
+
{
|
|
54
|
+
title: "Already Purchased",
|
|
55
|
+
message: "You already own this subscription. Restoring your purchase...",
|
|
56
|
+
shouldShowAlert: true,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
Purchases.PURCHASES_ERROR_CODE.PRODUCT_NOT_AVAILABLE_FOR_PURCHASE_ERROR,
|
|
61
|
+
{
|
|
62
|
+
title: "Product Unavailable",
|
|
63
|
+
message: "This product is not available for purchase at this time.",
|
|
64
|
+
shouldShowAlert: true,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR,
|
|
69
|
+
{
|
|
70
|
+
title: "Network Error",
|
|
71
|
+
message: "Please check your internet connection and try again.",
|
|
72
|
+
shouldShowAlert: true,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
Purchases.PURCHASES_ERROR_CODE.UNKNOWN_ERROR,
|
|
77
|
+
{
|
|
78
|
+
title: "Unknown Error",
|
|
79
|
+
message: "An unexpected error occurred. Please try again.",
|
|
80
|
+
shouldShowAlert: true,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
Purchases.PURCHASES_ERROR_CODE.RECEIPT_ALREADY_IN_USE_ERROR,
|
|
85
|
+
{
|
|
86
|
+
title: "Receipt Already Used",
|
|
87
|
+
message: "This receipt is already associated with another account.",
|
|
88
|
+
shouldShowAlert: true,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR,
|
|
93
|
+
{
|
|
94
|
+
title: "Configuration Error",
|
|
95
|
+
message: "The app is not configured correctly. Please contact support.",
|
|
96
|
+
shouldShowAlert: true,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
Purchases.PURCHASES_ERROR_CODE.UNEXPECTED_BACKEND_RESPONSE_ERROR,
|
|
101
|
+
{
|
|
102
|
+
title: "Server Error",
|
|
103
|
+
message: "The server returned an unexpected response. Please try again later.",
|
|
104
|
+
shouldShowAlert: true,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
Purchases.PURCHASES_ERROR_CODE.CONFIGURATION_ERROR,
|
|
109
|
+
{
|
|
110
|
+
title: "Configuration Error",
|
|
111
|
+
message: "RevenueCat is not configured correctly. Please contact support.",
|
|
112
|
+
shouldShowAlert: true,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
[
|
|
116
|
+
Purchases.PURCHASES_ERROR_CODE.STORE_PROBLEM_ERROR,
|
|
117
|
+
{
|
|
118
|
+
title: "Store Error",
|
|
119
|
+
message: "There was a problem with the app store. Please try again.",
|
|
120
|
+
shouldShowAlert: true,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
Purchases.PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR,
|
|
125
|
+
{
|
|
126
|
+
title: "Payment Pending",
|
|
127
|
+
message: "Your payment is still being processed. Please check back later.",
|
|
128
|
+
shouldShowAlert: true,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Default error message for unknown errors
|
|
135
|
+
*/
|
|
136
|
+
export const DEFAULT_ERROR_MESSAGE: ErrorMessage = {
|
|
137
|
+
title: "Error",
|
|
138
|
+
message: "An error occurred. Please try again.",
|
|
139
|
+
shouldShowAlert: true,
|
|
140
|
+
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { CustomerInfo } from "react-native-purchases";
|
|
2
|
-
import type { PurchaseSource } from "./SubscriptionConstants";
|
|
3
2
|
|
|
3
|
+
/**
|
|
4
|
+
* RevenueCat Configuration
|
|
5
|
+
* All callbacks receive data directly from RevenueCat SDK
|
|
6
|
+
*/
|
|
4
7
|
export interface RevenueCatConfig {
|
|
5
8
|
apiKey?: string;
|
|
6
9
|
entitlementIdentifier: string;
|
|
@@ -11,13 +14,13 @@ export interface RevenueCatConfig {
|
|
|
11
14
|
productId?: string,
|
|
12
15
|
expiresAt?: string,
|
|
13
16
|
willRenew?: boolean,
|
|
14
|
-
periodType?:
|
|
17
|
+
periodType?: string // From RevenueCat SDK (NORMAL, INTRO, TRIAL)
|
|
15
18
|
) => Promise<void> | void;
|
|
16
19
|
onPurchaseCompleted?: (
|
|
17
20
|
userId: string,
|
|
18
21
|
productId: string,
|
|
19
22
|
customerInfo: CustomerInfo,
|
|
20
|
-
source?:
|
|
23
|
+
source?: string // Purchase source tracking (app-specific)
|
|
21
24
|
) => Promise<void> | void;
|
|
22
25
|
onRestoreCompleted?: (
|
|
23
26
|
userId: string,
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import type { PeriodType } from "./SubscriptionConstants";
|
|
2
1
|
import type { Store, OwnershipType } from "./RevenueCatTypes";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* RevenueCat subscription data (Single Source of Truth)
|
|
6
5
|
* Used across the subscription package for storing RevenueCat data in Firestore
|
|
6
|
+
* All fields come directly from RevenueCat SDK - no manual definitions
|
|
7
7
|
*/
|
|
8
8
|
export interface RevenueCatData {
|
|
9
9
|
expirationDate: string | null;
|
|
10
10
|
willRenew: boolean | null;
|
|
11
11
|
originalTransactionId: string | null;
|
|
12
12
|
isPremium: boolean;
|
|
13
|
-
periodType:
|
|
13
|
+
periodType: string | null; // From RevenueCat SDK (NORMAL, INTRO, TRIAL)
|
|
14
14
|
unsubscribeDetectedAt: string | null;
|
|
15
15
|
billingIssueDetectedAt: string | null;
|
|
16
|
-
store: Store | null;
|
|
17
|
-
ownershipType: OwnershipType | null;
|
|
16
|
+
store: Store | null; // From PurchasesEntitlementInfo['store']
|
|
17
|
+
ownershipType: OwnershipType | null; // From PurchasesEntitlementInfo['ownershipType']
|
|
18
18
|
}
|
|
@@ -4,16 +4,21 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { CustomerInfo, PurchasesEntitlementInfo } from "react-native-purchases";
|
|
7
|
-
import { DEFAULT_ENTITLEMENT_ID } from "./SubscriptionConstants";
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
9
|
+
* Default entitlement identifier
|
|
10
|
+
* Can be overridden in RevenueCatConfig
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_ENTITLEMENT_ID = "premium";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Store type - Directly from RevenueCat SDK
|
|
11
16
|
* Automatically stays in sync with RevenueCat SDK updates
|
|
12
17
|
*/
|
|
13
18
|
export type Store = PurchasesEntitlementInfo['store'];
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
|
-
* OwnershipType
|
|
21
|
+
* OwnershipType - Directly from RevenueCat SDK
|
|
17
22
|
* Automatically stays in sync with RevenueCat SDK updates
|
|
18
23
|
*/
|
|
19
24
|
export type OwnershipType = PurchasesEntitlementInfo['ownershipType'];
|
|
@@ -132,10 +137,10 @@ export function isInvalidCredentialsError(error: unknown): boolean {
|
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
/**
|
|
135
|
-
* Extract error message
|
|
136
|
-
*
|
|
140
|
+
* Extract raw error message from error object
|
|
141
|
+
* For user-friendly messages, use getErrorMessageForCode from errors module
|
|
137
142
|
*/
|
|
138
|
-
export function
|
|
143
|
+
export function getRawErrorMessage(error: unknown, fallback: string): string {
|
|
139
144
|
if (error instanceof Error) {
|
|
140
145
|
return error.message;
|
|
141
146
|
}
|
package/src/domains/{subscription → revenuecat}/infrastructure/services/RevenueCatInitializer.ts
RENAMED
|
@@ -94,14 +94,23 @@ export async function initializeSDK(
|
|
|
94
94
|
try {
|
|
95
95
|
const currentAppUserId = await Purchases.getAppUserID();
|
|
96
96
|
let customerInfo;
|
|
97
|
+
|
|
98
|
+
// Handle user switching
|
|
97
99
|
if (currentAppUserId !== userId) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
if (userId) {
|
|
101
|
+
// Switch to authenticated user
|
|
102
|
+
const result = await Purchases.logIn(userId);
|
|
103
|
+
customerInfo = result.customerInfo;
|
|
104
|
+
} else {
|
|
105
|
+
// User logged out - switch to anonymous
|
|
106
|
+
customerInfo = await Purchases.logOut();
|
|
107
|
+
}
|
|
100
108
|
} else {
|
|
101
109
|
customerInfo = await Purchases.getCustomerInfo();
|
|
102
110
|
}
|
|
111
|
+
|
|
103
112
|
deps.setInitialized(true);
|
|
104
|
-
deps.setCurrentUserId(userId);
|
|
113
|
+
deps.setCurrentUserId(userId ?? null);
|
|
105
114
|
const offerings = await Purchases.getOfferings();
|
|
106
115
|
return buildSuccessResult(deps, customerInfo, offerings);
|
|
107
116
|
} catch {
|
|
@@ -133,9 +142,11 @@ export async function initializeSDK(
|
|
|
133
142
|
}
|
|
134
143
|
|
|
135
144
|
try {
|
|
136
|
-
|
|
145
|
+
// Configure with null appUserID for anonymous users (generates RevenueCat anonymous ID)
|
|
146
|
+
// For authenticated users, use their userId
|
|
147
|
+
await Purchases.configure({ apiKey: key, appUserID: userId ?? null });
|
|
137
148
|
deps.setInitialized(true);
|
|
138
|
-
deps.setCurrentUserId(userId);
|
|
149
|
+
deps.setCurrentUserId(userId ?? null);
|
|
139
150
|
|
|
140
151
|
const [customerInfo, offerings] = await Promise.all([
|
|
141
152
|
Purchases.getCustomerInfo(),
|
|
@@ -2,16 +2,25 @@ import type { FirebaseAuthLike } from "./SubscriptionInitializerTypes";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Gets the current user ID from Firebase auth.
|
|
5
|
+
* Returns undefined for anonymous users to prevent RevenueCat from using anonymous Firebase UIDs.
|
|
5
6
|
*/
|
|
6
7
|
export const getCurrentUserId = (getAuth: () => FirebaseAuthLike | null): string | undefined => {
|
|
7
8
|
const auth = getAuth();
|
|
8
9
|
if (!auth) return undefined;
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
const user = auth.currentUser;
|
|
12
|
+
if (!user) return undefined;
|
|
13
|
+
|
|
14
|
+
// Don't return userId for anonymous users - let RevenueCat use its own anonymous ID
|
|
15
|
+
if (user.isAnonymous) return undefined;
|
|
16
|
+
|
|
17
|
+
return user.uid;
|
|
10
18
|
};
|
|
11
19
|
|
|
12
20
|
/**
|
|
13
21
|
* Sets up auth state listener that will re-initialize subscription
|
|
14
22
|
* when user auth state changes (login/logout).
|
|
23
|
+
* Returns undefined for anonymous users to prevent RevenueCat from using anonymous Firebase UIDs.
|
|
15
24
|
*/
|
|
16
25
|
export const setupAuthStateListener = (
|
|
17
26
|
getAuth: () => FirebaseAuthLike | null,
|
|
@@ -21,6 +30,8 @@ export const setupAuthStateListener = (
|
|
|
21
30
|
if (!auth) return null;
|
|
22
31
|
|
|
23
32
|
return auth.onAuthStateChanged((user) => {
|
|
24
|
-
|
|
33
|
+
// Don't pass userId for anonymous users - let RevenueCat use its own anonymous ID
|
|
34
|
+
const userId = (user && !user.isAnonymous) ? user.uid : undefined;
|
|
35
|
+
onUserChange(userId);
|
|
25
36
|
});
|
|
26
37
|
};
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
import type { CreditsConfig } from "../../credits/core/Credits";
|
|
6
6
|
import type { UserCreditsDocumentRead } from "../../credits/core/UserCreditsDocument";
|
|
7
7
|
import type { PurchaseSource, PurchaseType } from "../core/SubscriptionConstants";
|
|
8
|
-
import type {
|
|
9
|
-
import type { Store, OwnershipType } from "../core/RevenueCatTypes";
|
|
8
|
+
import type { Store, OwnershipType } from "../../revenuecat/core/types";
|
|
10
9
|
|
|
11
10
|
export interface FirebaseAuthLike {
|
|
12
11
|
currentUser: { uid: string; isAnonymous: boolean } | null;
|
|
@@ -41,7 +40,7 @@ export interface InitializeCreditsMetadata {
|
|
|
41
40
|
willRenew: boolean | null;
|
|
42
41
|
originalTransactionId: string | null;
|
|
43
42
|
isPremium: boolean;
|
|
44
|
-
periodType:
|
|
43
|
+
periodType: string | null; // Raw value from RevenueCat SDK
|
|
45
44
|
unsubscribeDetectedAt: string | null;
|
|
46
45
|
billingIssueDetectedAt: string | null;
|
|
47
46
|
store: Store | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CustomerInfo } from "react-native-purchases";
|
|
2
|
-
import type { RevenueCatData } from "
|
|
2
|
+
import type { RevenueCatData } from "../../revenuecat/core/types";
|
|
3
3
|
import { PERIOD_TYPE, type PeriodType } from "../core/SubscriptionStatus";
|
|
4
4
|
|
|
5
5
|
function validatePeriodType(periodType: string | undefined): PeriodType | null {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RevenueCatData } from "
|
|
1
|
+
import type { RevenueCatData } from "../../revenuecat/core/types";
|
|
2
2
|
import type { PeriodType } from "../core/SubscriptionConstants";
|
|
3
3
|
import { PURCHASE_SOURCE, PURCHASE_TYPE } from "../core/SubscriptionConstants";
|
|
4
4
|
import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
|
|
@@ -26,7 +26,7 @@ export interface SubscriptionStatus {
|
|
|
26
26
|
customerId?: string | null;
|
|
27
27
|
syncedAt?: string | null;
|
|
28
28
|
status?: SubscriptionStatusType;
|
|
29
|
-
periodType?:
|
|
29
|
+
periodType?: string; // Raw value from RevenueCat SDK (NORMAL, INTRO, TRIAL)
|
|
30
30
|
isTrialing?: boolean;
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -55,7 +55,7 @@ export interface StatusResolverInput {
|
|
|
55
55
|
isPremium: boolean;
|
|
56
56
|
willRenew?: boolean;
|
|
57
57
|
isExpired?: boolean;
|
|
58
|
-
periodType?:
|
|
58
|
+
periodType?: string; // Raw value from RevenueCat SDK (NORMAL, INTRO, TRIAL)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// Singleton Chain Instance
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { PurchasesPackage, CustomerInfo } from "react-native-purchases";
|
|
7
7
|
import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
|
|
8
|
-
import { getPremiumEntitlement } from "
|
|
8
|
+
import { getPremiumEntitlement } from "../../../revenuecat/core/types";
|
|
9
9
|
import { PurchaseStatusResolver, type PremiumStatus } from "./PurchaseStatusResolver";
|
|
10
10
|
|
|
11
11
|
export interface RestoreResultInfo {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CustomerInfo } from "react-native-purchases";
|
|
2
|
-
import { getPremiumEntitlement } from "
|
|
2
|
+
import { getPremiumEntitlement } from "../../../revenuecat/core/types";
|
|
3
3
|
import { toDate } from "../../../../shared/utils/dateConverter";
|
|
4
4
|
|
|
5
5
|
export interface PremiumStatus {
|
|
@@ -6,18 +6,18 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { useMutation, useQueryClient } from "@umituz/react-native-design-system";
|
|
9
|
-
import
|
|
9
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
10
10
|
import { useAlert } from "@umituz/react-native-design-system";
|
|
11
11
|
import {
|
|
12
12
|
useAuthStore,
|
|
13
13
|
selectUserId,
|
|
14
|
+
selectIsAnonymous,
|
|
14
15
|
} from "@umituz/react-native-auth";
|
|
15
16
|
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
16
17
|
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
17
18
|
import { subscriptionStatusQueryKeys } from "../../presentation/useSubscriptionStatus";
|
|
18
19
|
import { creditsQueryKeys } from "../../../credits/presentation/creditsQueryKeys";
|
|
19
|
-
import {
|
|
20
|
-
import type { RevenueCatPurchaseErrorInfo } from "../../core/RevenueCatTypes";
|
|
20
|
+
import { getErrorMessage } from "../../../revenuecat/core/errors";
|
|
21
21
|
|
|
22
22
|
/** Purchase mutation result - simplified for presentation layer */
|
|
23
23
|
export interface PurchaseMutationResult {
|
|
@@ -33,6 +33,7 @@ export interface PurchaseMutationResult {
|
|
|
33
33
|
*/
|
|
34
34
|
export const usePurchasePackage = () => {
|
|
35
35
|
const userId = useAuthStore(selectUserId);
|
|
36
|
+
const isAnonymous = useAuthStore(selectIsAnonymous);
|
|
36
37
|
const queryClient = useQueryClient();
|
|
37
38
|
const { showSuccess, showError } = useAlert();
|
|
38
39
|
|
|
@@ -42,6 +43,10 @@ export const usePurchasePackage = () => {
|
|
|
42
43
|
throw new Error("User not authenticated");
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
if (isAnonymous) {
|
|
47
|
+
throw new Error("Anonymous users cannot purchase subscriptions");
|
|
48
|
+
}
|
|
49
|
+
|
|
45
50
|
const productId = pkg.product.identifier;
|
|
46
51
|
const success = await SubscriptionManager.purchasePackage(pkg);
|
|
47
52
|
|
|
@@ -66,60 +71,15 @@ export const usePurchasePackage = () => {
|
|
|
66
71
|
}
|
|
67
72
|
},
|
|
68
73
|
onError: (error) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (error instanceof Error) {
|
|
73
|
-
// Type assertion for RevenueCat error
|
|
74
|
-
const rcError = error as RevenueCatPurchaseErrorInfo;
|
|
75
|
-
const errorCode = rcError.code || rcError.readableErrorCode;
|
|
74
|
+
// Use map-based lookup - O(1) complexity
|
|
75
|
+
const errorInfo = getErrorMessage(error);
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
title = errorInfo.title;
|
|
81
|
-
message = errorInfo.message;
|
|
82
|
-
} else {
|
|
83
|
-
// Fallback to specific error code checks using Purchases enum
|
|
84
|
-
const code = errorCode;
|
|
85
|
-
|
|
86
|
-
if (code === Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
|
|
87
|
-
return; // Don't show error for user cancellation
|
|
88
|
-
} else if (code === Purchases.PURCHASES_ERROR_CODE.PURCHASE_NOT_ALLOWED_ERROR) {
|
|
89
|
-
title = "Purchase Not Allowed";
|
|
90
|
-
message = "In-app purchases are disabled on this device.";
|
|
91
|
-
} else if (code === Purchases.PURCHASES_ERROR_CODE.PURCHASE_INVALID_ERROR) {
|
|
92
|
-
title = "Invalid Purchase";
|
|
93
|
-
message = "The purchase is invalid. Please contact support.";
|
|
94
|
-
} else if (code === Purchases.PURCHASES_ERROR_CODE.PRODUCT_ALREADY_PURCHASED_ERROR) {
|
|
95
|
-
title = "Already Purchased";
|
|
96
|
-
message = "You already own this subscription. Restoring...";
|
|
97
|
-
} else if (code === Purchases.PURCHASES_ERROR_CODE.PRODUCT_NOT_AVAILABLE_FOR_PURCHASE_ERROR) {
|
|
98
|
-
title = "Product Unavailable";
|
|
99
|
-
message = "This product is not available for purchase.";
|
|
100
|
-
} else if (code === Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR) {
|
|
101
|
-
title = "Network Error";
|
|
102
|
-
message = "Please check your internet connection and try again.";
|
|
103
|
-
} else if (code === Purchases.PURCHASES_ERROR_CODE.RECEIPT_ALREADY_IN_USE_ERROR) {
|
|
104
|
-
title = "Receipt Already Used";
|
|
105
|
-
message = "This receipt is already associated with another account.";
|
|
106
|
-
} else if (code === Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR) {
|
|
107
|
-
title = "Configuration Error";
|
|
108
|
-
message = "App is not configured correctly. Please contact support.";
|
|
109
|
-
} else if (code === Purchases.PURCHASES_ERROR_CODE.UNEXPECTED_BACKEND_RESPONSE_ERROR) {
|
|
110
|
-
title = "Server Error";
|
|
111
|
-
message = "The server returned an unexpected response. Please try again later.";
|
|
112
|
-
} else if (code === Purchases.PURCHASES_ERROR_CODE.CONFIGURATION_ERROR) {
|
|
113
|
-
title = "Configuration Error";
|
|
114
|
-
message = "RevenueCat is not configured correctly. Please contact support.";
|
|
115
|
-
} else {
|
|
116
|
-
// Use error message if no specific code matched
|
|
117
|
-
message = error.message || message;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
77
|
+
// Don't show alert for user cancellation
|
|
78
|
+
if (!errorInfo.shouldShowAlert) {
|
|
79
|
+
return;
|
|
120
80
|
}
|
|
121
81
|
|
|
122
|
-
showError(title, message);
|
|
82
|
+
showError(errorInfo.title, errorInfo.message);
|
|
123
83
|
},
|
|
124
84
|
});
|
|
125
85
|
};
|
|
@@ -6,18 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { useMutation, useQueryClient } from "@umituz/react-native-design-system";
|
|
9
|
-
import Purchases from "react-native-purchases";
|
|
10
9
|
import { useAlert } from "@umituz/react-native-design-system";
|
|
11
10
|
import {
|
|
12
11
|
useAuthStore,
|
|
13
12
|
selectUserId,
|
|
13
|
+
selectIsAnonymous,
|
|
14
14
|
} from "@umituz/react-native-auth";
|
|
15
15
|
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
16
16
|
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
17
17
|
import { subscriptionStatusQueryKeys } from "../../presentation/useSubscriptionStatus";
|
|
18
18
|
import { creditsQueryKeys } from "../../../credits/presentation/creditsQueryKeys";
|
|
19
|
-
import {
|
|
20
|
-
import type { RevenueCatPurchaseErrorInfo } from "../../core/RevenueCatTypes";
|
|
19
|
+
import { getErrorMessage } from "../../../revenuecat/core/errors";
|
|
21
20
|
|
|
22
21
|
interface RestoreResult {
|
|
23
22
|
success: boolean;
|
|
@@ -31,6 +30,7 @@ interface RestoreResult {
|
|
|
31
30
|
*/
|
|
32
31
|
export const useRestorePurchase = () => {
|
|
33
32
|
const userId = useAuthStore(selectUserId);
|
|
33
|
+
const isAnonymous = useAuthStore(selectIsAnonymous);
|
|
34
34
|
const queryClient = useQueryClient();
|
|
35
35
|
const { showSuccess, showInfo, showError } = useAlert();
|
|
36
36
|
|
|
@@ -40,6 +40,10 @@ export const useRestorePurchase = () => {
|
|
|
40
40
|
throw new Error("User not authenticated");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
if (isAnonymous) {
|
|
44
|
+
throw new Error("Anonymous users cannot restore purchases");
|
|
45
|
+
}
|
|
46
|
+
|
|
43
47
|
const result = await SubscriptionManager.restore();
|
|
44
48
|
return result;
|
|
45
49
|
},
|
|
@@ -67,43 +71,9 @@ export const useRestorePurchase = () => {
|
|
|
67
71
|
}
|
|
68
72
|
},
|
|
69
73
|
onError: (error) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (error instanceof Error) {
|
|
74
|
-
// Type assertion for RevenueCat error
|
|
75
|
-
const rcError = error as RevenueCatPurchaseErrorInfo;
|
|
76
|
-
const errorCode = rcError.code || rcError.readableErrorCode;
|
|
77
|
-
|
|
78
|
-
// Get user-friendly message from constants if available
|
|
79
|
-
if (errorCode && errorCode in ERROR_MESSAGES) {
|
|
80
|
-
const errorInfo = ERROR_MESSAGES[errorCode];
|
|
81
|
-
title = errorInfo.title;
|
|
82
|
-
message = errorInfo.message;
|
|
83
|
-
} else {
|
|
84
|
-
// Fallback to specific error code checks
|
|
85
|
-
const code = errorCode;
|
|
86
|
-
|
|
87
|
-
if (code === Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR) {
|
|
88
|
-
title = "Network Error";
|
|
89
|
-
message = "Please check your internet connection and try again.";
|
|
90
|
-
} else if (code === Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR) {
|
|
91
|
-
title = "Configuration Error";
|
|
92
|
-
message = "App is not configured correctly. Please contact support.";
|
|
93
|
-
} else if (code === Purchases.PURCHASES_ERROR_CODE.UNEXPECTED_BACKEND_RESPONSE_ERROR) {
|
|
94
|
-
title = "Server Error";
|
|
95
|
-
message = "The server returned an unexpected response. Please try again later.";
|
|
96
|
-
} else if (code === Purchases.PURCHASES_ERROR_CODE.CONFIGURATION_ERROR) {
|
|
97
|
-
title = "Configuration Error";
|
|
98
|
-
message = "RevenueCat is not configured correctly. Please contact support.";
|
|
99
|
-
} else {
|
|
100
|
-
// Use error message if no specific code matched
|
|
101
|
-
message = error.message || message;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
showError(title, message);
|
|
74
|
+
// Use map-based lookup - O(1) complexity
|
|
75
|
+
const errorInfo = getErrorMessage(error);
|
|
76
|
+
showError(errorInfo.title, errorInfo.message);
|
|
107
77
|
},
|
|
108
78
|
});
|
|
109
79
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
|
|
2
2
|
import { initializeRevenueCatService, getRevenueCatService } from "../services/RevenueCatService";
|
|
3
3
|
import { ensureServiceAvailable } from "./subscriptionManagerUtils";
|
|
4
|
-
import type { RevenueCatConfig } from "
|
|
4
|
+
import type { RevenueCatConfig } from "../../../revenuecat/core/types";
|
|
5
5
|
|
|
6
6
|
export const performServiceInitialization = async (config: RevenueCatConfig, userId: string): Promise<{ service: IRevenueCatService; success: boolean }> => {
|
|
7
7
|
await initializeRevenueCatService(config);
|
|
@@ -7,7 +7,7 @@ import Purchases, {
|
|
|
7
7
|
type CustomerInfo,
|
|
8
8
|
type CustomerInfoUpdateListener,
|
|
9
9
|
} from "react-native-purchases";
|
|
10
|
-
import type { RevenueCatConfig } from "
|
|
10
|
+
import type { RevenueCatConfig } from "../../../revenuecat/core/types";
|
|
11
11
|
import { syncPremiumStatus } from "../utils/PremiumStatusSyncer";
|
|
12
12
|
import {
|
|
13
13
|
detectRenewal,
|
|
@@ -4,16 +4,16 @@ import {
|
|
|
4
4
|
RevenueCatPurchaseError,
|
|
5
5
|
RevenueCatInitializationError,
|
|
6
6
|
RevenueCatNetworkError,
|
|
7
|
-
} from "
|
|
8
|
-
import type { RevenueCatConfig } from "
|
|
7
|
+
} from "../../../revenuecat/core/errors";
|
|
8
|
+
import type { RevenueCatConfig } from "../../../revenuecat/core/types";
|
|
9
9
|
import {
|
|
10
10
|
isUserCancelledError,
|
|
11
11
|
isNetworkError,
|
|
12
12
|
isAlreadyPurchasedError,
|
|
13
13
|
isInvalidCredentialsError,
|
|
14
|
-
|
|
14
|
+
getRawErrorMessage,
|
|
15
15
|
getErrorCode,
|
|
16
|
-
} from "
|
|
16
|
+
} from "../../../revenuecat/core/types";
|
|
17
17
|
import { syncPremiumStatus, notifyPurchaseCompleted } from "../utils/PremiumStatusSyncer";
|
|
18
18
|
import { getSavedPurchase, clearSavedPurchase } from "../../presentation/useAuthAwarePurchase";
|
|
19
19
|
import { handleRestore } from "./RestoreHandler";
|
|
@@ -120,7 +120,7 @@ export async function handlePurchase(
|
|
|
120
120
|
|
|
121
121
|
// Generic error with code
|
|
122
122
|
const errorCode = getErrorCode(error);
|
|
123
|
-
const errorMessage =
|
|
123
|
+
const errorMessage = getRawErrorMessage(error, "Purchase failed");
|
|
124
124
|
const enhancedMessage = errorCode
|
|
125
125
|
? `${errorMessage} (Code: ${errorCode})`
|
|
126
126
|
: errorMessage;
|
|
@@ -4,14 +4,14 @@ import {
|
|
|
4
4
|
RevenueCatRestoreError,
|
|
5
5
|
RevenueCatInitializationError,
|
|
6
6
|
RevenueCatNetworkError,
|
|
7
|
-
} from "
|
|
8
|
-
import type { RevenueCatConfig } from "
|
|
7
|
+
} from "../../../revenuecat/core/errors";
|
|
8
|
+
import type { RevenueCatConfig } from "../../../revenuecat/core/types";
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
getRawErrorMessage,
|
|
11
11
|
getErrorCode,
|
|
12
12
|
isNetworkError,
|
|
13
13
|
isInvalidCredentialsError,
|
|
14
|
-
} from "
|
|
14
|
+
} from "../../../revenuecat/core/types";
|
|
15
15
|
import { syncPremiumStatus, notifyRestoreCompleted } from "../utils/PremiumStatusSyncer";
|
|
16
16
|
|
|
17
17
|
export interface RestoreHandlerDeps {
|
|
@@ -53,7 +53,7 @@ export async function handleRestore(deps: RestoreHandlerDeps, userId: string): P
|
|
|
53
53
|
|
|
54
54
|
// Generic error with code
|
|
55
55
|
const errorCode = getErrorCode(error);
|
|
56
|
-
const errorMessage =
|
|
56
|
+
const errorMessage = getRawErrorMessage(error, "Restore failed");
|
|
57
57
|
const enhancedMessage = errorCode
|
|
58
58
|
? `${errorMessage} (Code: ${errorCode})`
|
|
59
59
|
: errorMessage;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import Purchases from "react-native-purchases";
|
|
2
2
|
import type { PurchasesOffering, PurchasesPackage, CustomerInfo } from "react-native-purchases";
|
|
3
3
|
import type { IRevenueCatService, InitializeResult, PurchaseResult, RestoreResult } from "../../../../shared/application/ports/IRevenueCatService";
|
|
4
|
-
import type { RevenueCatConfig } from "
|
|
5
|
-
import { resolveApiKey } from "
|
|
6
|
-
import { initializeSDK } from "
|
|
4
|
+
import type { RevenueCatConfig } from "../../../revenuecat/core/types";
|
|
5
|
+
import { resolveApiKey } from "../../../revenuecat/infrastructure/utils/ApiKeyResolver";
|
|
6
|
+
import { initializeSDK } from "../../../revenuecat/infrastructure/services/RevenueCatInitializer";
|
|
7
7
|
import { fetchOfferings } from "./OfferingsFetcher";
|
|
8
8
|
import { handlePurchase } from "./PurchaseHandler";
|
|
9
9
|
import { handleRestore } from "./RestoreHandler";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RevenueCatService } from "./RevenueCatService.types";
|
|
2
|
-
import type { RevenueCatConfig } from "
|
|
2
|
+
import type { RevenueCatConfig } from "../../../revenuecat/core/types";
|
|
3
3
|
|
|
4
4
|
let revenueCatServiceInstance: RevenueCatService | null = null;
|
|
5
5
|
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { CustomerInfo } from "react-native-purchases";
|
|
7
|
-
import type { RevenueCatConfig } from "
|
|
7
|
+
import type { RevenueCatConfig } from "../../../revenuecat/core/types";
|
|
8
8
|
import type { PurchaseSource } from "../../../subscription/core/SubscriptionConstants";
|
|
9
|
-
import { getPremiumEntitlement } from "
|
|
9
|
+
import { getPremiumEntitlement } from "../../../revenuecat/core/types";
|
|
10
10
|
|
|
11
11
|
export async function syncPremiumStatus(
|
|
12
12
|
config: RevenueCatConfig,
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RevenueCat Constants
|
|
3
|
-
* Error codes, messages, and logging constants
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import Purchases from "react-native-purchases";
|
|
7
|
-
|
|
8
|
-
export const REVENUECAT_LOG_PREFIX = "[RevenueCat]";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* RevenueCat Error Code Type
|
|
12
|
-
* Re-export for type safety
|
|
13
|
-
*/
|
|
14
|
-
export type PurchasesErrorCode = typeof Purchases.PURCHASES_ERROR_CODE[keyof typeof Purchases.PURCHASES_ERROR_CODE];
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* User-friendly error messages for RevenueCat errors
|
|
18
|
-
* Maps error codes to human-readable messages
|
|
19
|
-
*/
|
|
20
|
-
export const ERROR_MESSAGES: Record<string, { title: string; message: string }> = {
|
|
21
|
-
PURCHASE_CANCELLED_ERROR: {
|
|
22
|
-
title: "Purchase Cancelled",
|
|
23
|
-
message: "The purchase was cancelled.",
|
|
24
|
-
},
|
|
25
|
-
PURCHASE_NOT_ALLOWED_ERROR: {
|
|
26
|
-
title: "Purchase Not Allowed",
|
|
27
|
-
message: "In-app purchases are disabled on this device.",
|
|
28
|
-
},
|
|
29
|
-
PURCHASE_INVALID_ERROR: {
|
|
30
|
-
title: "Invalid Purchase",
|
|
31
|
-
message: "The purchase is invalid. Please contact support.",
|
|
32
|
-
},
|
|
33
|
-
PRODUCT_ALREADY_PURCHASED_ERROR: {
|
|
34
|
-
title: "Already Purchased",
|
|
35
|
-
message: "You already own this subscription. Restoring your purchase...",
|
|
36
|
-
},
|
|
37
|
-
PRODUCT_NOT_AVAILABLE_FOR_PURCHASE_ERROR: {
|
|
38
|
-
title: "Product Unavailable",
|
|
39
|
-
message: "This product is not available for purchase at this time.",
|
|
40
|
-
},
|
|
41
|
-
NETWORK_ERROR: {
|
|
42
|
-
title: "Network Error",
|
|
43
|
-
message: "Please check your internet connection and try again.",
|
|
44
|
-
},
|
|
45
|
-
UNKNOWN_ERROR: {
|
|
46
|
-
title: "Unknown Error",
|
|
47
|
-
message: "An unexpected error occurred. Please try again.",
|
|
48
|
-
},
|
|
49
|
-
RECEIPT_ALREADY_IN_USE_ERROR: {
|
|
50
|
-
title: "Receipt Already Used",
|
|
51
|
-
message: "This receipt is already associated with another account.",
|
|
52
|
-
},
|
|
53
|
-
INVALID_CREDENTIALS_ERROR: {
|
|
54
|
-
title: "Configuration Error",
|
|
55
|
-
message: "The app is not configured correctly. Please contact support.",
|
|
56
|
-
},
|
|
57
|
-
UNEXPECTED_BACKEND_RESPONSE_ERROR: {
|
|
58
|
-
title: "Server Error",
|
|
59
|
-
message: "The server returned an unexpected response. Please try again later.",
|
|
60
|
-
},
|
|
61
|
-
CONFIGURATION_ERROR: {
|
|
62
|
-
title: "Configuration Error",
|
|
63
|
-
message: "RevenueCat is not configured correctly. Please contact support.",
|
|
64
|
-
},
|
|
65
|
-
STORE_PROBLEM_ERROR: {
|
|
66
|
-
title: "Store Error",
|
|
67
|
-
message: "There was a problem with the app store. Please try again.",
|
|
68
|
-
},
|
|
69
|
-
PAYMENT_PENDING_ERROR: {
|
|
70
|
-
title: "Payment Pending",
|
|
71
|
-
message: "Your payment is still being processed. Please check back later.",
|
|
72
|
-
},
|
|
73
|
-
};
|
/package/src/domains/{subscription → revenuecat}/infrastructure/services/initializerConstants.ts
RENAMED
|
File without changes
|