vibefast-cli 0.5.0 → 0.5.1
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/FEATURE-DEPENDENCY-SPEC.md +338 -0
- package/dist/__tests__/integration.test.d.ts +2 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +219 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/recipes.test.d.ts +2 -0
- package/dist/__tests__/recipes.test.d.ts.map +1 -0
- package/dist/__tests__/recipes.test.js +143 -0
- package/dist/__tests__/recipes.test.js.map +1 -0
- package/dist/commands/__tests__/init.test.d.ts +2 -0
- package/dist/commands/__tests__/init.test.d.ts.map +1 -0
- package/dist/commands/__tests__/init.test.js +95 -0
- package/dist/commands/__tests__/init.test.js.map +1 -0
- package/dist/commands/__tests__/platform.test.d.ts +2 -0
- package/dist/commands/__tests__/platform.test.d.ts.map +1 -0
- package/dist/commands/__tests__/platform.test.js +123 -0
- package/dist/commands/__tests__/platform.test.js.map +1 -0
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +4 -5
- package/dist/commands/add.js.map +1 -1
- package/dist/core/journal.d.ts.map +1 -1
- package/dist/core/journal.js +36 -19
- package/dist/core/journal.js.map +1 -1
- package/dist/core/recipes.d.ts.map +1 -1
- package/dist/core/recipes.js +8 -39
- package/dist/core/recipes.js.map +1 -1
- package/package.json +1 -1
- package/recipes/ios-widget/recipe.json +78 -0
- package/recipes/ios-widget/targets/widget/AppIntent.swift +46 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
- package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png +0 -0
- package/recipes/ios-widget/targets/widget/CalorieTrackerWidget.swift +424 -0
- package/recipes/ios-widget/targets/widget/HabitTrackerWidget.swift +305 -0
- package/recipes/ios-widget/targets/widget/Info.plist +11 -0
- package/recipes/ios-widget/targets/widget/WidgetLiveActivity.swift +75 -0
- package/recipes/ios-widget/targets/widget/expo-target.config.js +10 -0
- package/recipes/ios-widget/targets/widget/generated.entitlements +5 -0
- package/recipes/ios-widget/targets/widget/index.swift +18 -0
- package/recipes/ios-widget/targets/widget/widgets.swift +96 -0
- package/recipes/ios-widget@latest.zip +0 -0
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +74 -0
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +25 -0
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +23 -0
- package/recipes/payments/apps/native/src/features/payments/README.md +200 -0
- package/recipes/payments/apps/native/src/features/payments/app/local-paywall.tsx +194 -0
- package/recipes/payments/apps/native/src/features/payments/app/remote-paywall.tsx +79 -0
- package/recipes/payments/apps/native/src/features/payments/components/payment-initializer.tsx +95 -0
- package/recipes/payments/apps/native/src/features/payments/components/paywall-error-state.tsx +60 -0
- package/recipes/payments/apps/native/src/features/payments/components/paywall-local-mode.tsx +116 -0
- package/recipes/payments/apps/native/src/features/payments/components/paywall-product-card.tsx +133 -0
- package/recipes/payments/apps/native/src/features/payments/components/paywall-remote-mode.tsx +146 -0
- package/recipes/payments/apps/native/src/features/payments/hooks/use-entitlement.ts +63 -0
- package/recipes/payments/apps/native/src/features/payments/index.ts +8 -0
- package/recipes/payments/apps/native/src/features/payments/services/revenuecat-adapter.ts +407 -0
- package/recipes/payments/recipe.json +58 -0
- package/recipes/payments@latest.zip +0 -0
- package/src/__tests__/integration.test.ts +249 -0
- package/src/__tests__/recipes.test.ts +168 -0
- package/src/commands/__tests__/init.test.ts +112 -0
- package/src/commands/__tests__/platform.test.ts +141 -0
- package/src/commands/add.ts +4 -5
- package/src/core/journal.ts +42 -25
- package/src/core/recipes.ts +8 -40
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import Purchases, {
|
|
3
|
+
LOG_LEVEL,
|
|
4
|
+
type PurchasesOffering,
|
|
5
|
+
type PurchasesPackage,
|
|
6
|
+
} from 'react-native-purchases';
|
|
7
|
+
|
|
8
|
+
import { ConfigService } from '@/core/config';
|
|
9
|
+
import * as Logger from '@/core/logging';
|
|
10
|
+
import type {
|
|
11
|
+
PaymentService,
|
|
12
|
+
ProductOffering,
|
|
13
|
+
} from '@/core/payments/payment-service';
|
|
14
|
+
|
|
15
|
+
// Ensure a log handler is registered ASAP to avoid
|
|
16
|
+
// "TypeError: customLogHandler is not a function" on Android Hermes
|
|
17
|
+
try {
|
|
18
|
+
// If a handler is already set later via configure, this will be overwritten.
|
|
19
|
+
Purchases.setLogHandler((logLevel, message) => {
|
|
20
|
+
switch (logLevel) {
|
|
21
|
+
case LOG_LEVEL.DEBUG:
|
|
22
|
+
// console.debug(`[RevenueCat] ${message}`);
|
|
23
|
+
break;
|
|
24
|
+
case LOG_LEVEL.INFO:
|
|
25
|
+
// console.info(`[RevenueCat] ${message}`);
|
|
26
|
+
break;
|
|
27
|
+
case LOG_LEVEL.WARN:
|
|
28
|
+
// console.warn(`[RevenueCat] ${message}`);
|
|
29
|
+
break;
|
|
30
|
+
case LOG_LEVEL.ERROR:
|
|
31
|
+
// console.error(`[RevenueCat] ${message}`);
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
// console.log(`[RevenueCat] ${message}`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
// best-effort: ignore if native module isn't ready yet
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* RevenueCat adapter service implementing the PaymentService interface
|
|
43
|
+
*/
|
|
44
|
+
export class RevenueCatAdapter implements PaymentService {
|
|
45
|
+
private static isInitialized = false;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize the RevenueCat SDK
|
|
49
|
+
* Should be called once during app startup
|
|
50
|
+
*/
|
|
51
|
+
static async initialize(): Promise<void> {
|
|
52
|
+
if (RevenueCatAdapter.isInitialized) {
|
|
53
|
+
Logger.info('[RevenueCatAdapter] SDK already initialized, skipping');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Check if RevenueCat is already configured (Fast Refresh protection)
|
|
59
|
+
try {
|
|
60
|
+
await Purchases.getCustomerInfo();
|
|
61
|
+
Logger.info(
|
|
62
|
+
'[RevenueCatAdapter] RevenueCat already configured from previous session',
|
|
63
|
+
);
|
|
64
|
+
RevenueCatAdapter.isInitialized = true;
|
|
65
|
+
return;
|
|
66
|
+
} catch {
|
|
67
|
+
// Not configured yet, proceed with configuration
|
|
68
|
+
Logger.debug(
|
|
69
|
+
'[RevenueCatAdapter] RevenueCat not configured, proceeding with initialization',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Get the appropriate API key based on platform
|
|
74
|
+
const apiKey = Platform.select({
|
|
75
|
+
ios: ConfigService.getRevenueCatApiKeyApple(),
|
|
76
|
+
android: ConfigService.getRevenueCatApiKeyGoogle(),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!apiKey) {
|
|
80
|
+
Logger.warn(
|
|
81
|
+
`[RevenueCatAdapter] No API key found for platform: ${Platform.OS}`,
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Set debug logs level (only in development)
|
|
87
|
+
if (ConfigService.getAppEnv() !== 'production') {
|
|
88
|
+
Purchases.setLogLevel(LOG_LEVEL.DEBUG);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Configure and initialize the SDK
|
|
92
|
+
await Purchases.configure({ apiKey });
|
|
93
|
+
|
|
94
|
+
RevenueCatAdapter.isInitialized = true;
|
|
95
|
+
Logger.info('[RevenueCatAdapter] SDK initialized successfully');
|
|
96
|
+
} catch (error) {
|
|
97
|
+
Logger.error(
|
|
98
|
+
'[RevenueCatAdapter] Failed to initialize SDK: ' + String(error),
|
|
99
|
+
);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if the SDK is initialized
|
|
106
|
+
*/
|
|
107
|
+
static getIsInitialized(): boolean {
|
|
108
|
+
return RevenueCatAdapter.isInitialized;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Fetch product offerings from RevenueCat
|
|
113
|
+
*/
|
|
114
|
+
async fetchProductOfferings(
|
|
115
|
+
_configType: 'local' | 'remote',
|
|
116
|
+
): Promise<ProductOffering[]> {
|
|
117
|
+
this.ensureInitialized();
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const offerings = await Purchases.getOfferings();
|
|
121
|
+
Logger.debug('[RevenueCatAdapter] Offerings retrieved');
|
|
122
|
+
|
|
123
|
+
const productOfferings: ProductOffering[] = [];
|
|
124
|
+
|
|
125
|
+
// Process current offering
|
|
126
|
+
if (offerings.current) {
|
|
127
|
+
const currentOffering = offerings.current;
|
|
128
|
+
|
|
129
|
+
// Convert packages to ProductOffering format
|
|
130
|
+
Object.values(currentOffering.availablePackages).forEach(
|
|
131
|
+
(pkg: PurchasesPackage) => {
|
|
132
|
+
productOfferings.push({
|
|
133
|
+
id: pkg.identifier,
|
|
134
|
+
priceString: pkg.product.priceString,
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Process all offerings if needed
|
|
141
|
+
Object.values(offerings.all).forEach((offering: PurchasesOffering) => {
|
|
142
|
+
Object.values(offering.availablePackages).forEach(
|
|
143
|
+
(pkg: PurchasesPackage) => {
|
|
144
|
+
// Avoid duplicates
|
|
145
|
+
if (!productOfferings.find((p) => p.id === pkg.identifier)) {
|
|
146
|
+
productOfferings.push({
|
|
147
|
+
id: pkg.identifier,
|
|
148
|
+
priceString: pkg.product.priceString,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return productOfferings;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
Logger.error(
|
|
158
|
+
'[RevenueCatAdapter] Failed to fetch offerings: ' + String(error),
|
|
159
|
+
);
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Purchase a product
|
|
166
|
+
*/
|
|
167
|
+
async purchaseProduct(
|
|
168
|
+
productId: string,
|
|
169
|
+
offeringIdentifier?: string,
|
|
170
|
+
onConsumablePurchase?: (details: {
|
|
171
|
+
productId: string;
|
|
172
|
+
quantity: number;
|
|
173
|
+
}) => Promise<void>,
|
|
174
|
+
): Promise<{ success: boolean; error?: string; customerInfo?: any }> {
|
|
175
|
+
this.ensureInitialized();
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
// Get offerings to find the package
|
|
179
|
+
const offerings = await Purchases.getOfferings();
|
|
180
|
+
let packageToPurchase: PurchasesPackage | null = null;
|
|
181
|
+
|
|
182
|
+
// Find the package by productId
|
|
183
|
+
if (offeringIdentifier && offerings.all[offeringIdentifier]) {
|
|
184
|
+
packageToPurchase =
|
|
185
|
+
offerings.all[offeringIdentifier].availablePackages.find(
|
|
186
|
+
(pkg) => pkg.identifier === productId,
|
|
187
|
+
) || null;
|
|
188
|
+
} else if (offerings.current) {
|
|
189
|
+
packageToPurchase =
|
|
190
|
+
offerings.current.availablePackages.find(
|
|
191
|
+
(pkg) => pkg.identifier === productId,
|
|
192
|
+
) || null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!packageToPurchase) {
|
|
196
|
+
return {
|
|
197
|
+
success: false,
|
|
198
|
+
error: `Package with ID ${productId} not found`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const { customerInfo } =
|
|
203
|
+
await Purchases.purchasePackage(packageToPurchase);
|
|
204
|
+
Logger.info(`[RevenueCatAdapter] Purchase successful: ${productId}`);
|
|
205
|
+
|
|
206
|
+
// Check if this is a consumable product and handle it
|
|
207
|
+
if (onConsumablePurchase && this.isConsumableProduct(productId)) {
|
|
208
|
+
const quantity = this.getConsumableQuantity(productId);
|
|
209
|
+
try {
|
|
210
|
+
await onConsumablePurchase({ productId, quantity });
|
|
211
|
+
Logger.info(
|
|
212
|
+
`[RevenueCatAdapter] Consumable purchase recorded: ${productId} (${quantity} credits)`,
|
|
213
|
+
);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
Logger.error(
|
|
216
|
+
`[RevenueCatAdapter] Failed to record consumable purchase: ${String(error)}`,
|
|
217
|
+
);
|
|
218
|
+
// Continue with successful purchase response even if backend recording fails
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
success: true,
|
|
224
|
+
customerInfo,
|
|
225
|
+
};
|
|
226
|
+
} catch (error) {
|
|
227
|
+
Logger.error('[RevenueCatAdapter] Purchase failed: ' + String(error));
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
error: String(error),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get user subscription status
|
|
237
|
+
*/
|
|
238
|
+
async getUserSubscriptionStatus(
|
|
239
|
+
entitlementId: string,
|
|
240
|
+
): Promise<{ isActive: boolean; details?: any }> {
|
|
241
|
+
this.ensureInitialized();
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const customerInfo = await Purchases.getCustomerInfo();
|
|
245
|
+
const entitlement = customerInfo.entitlements.active[entitlementId];
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
isActive: !!entitlement,
|
|
249
|
+
details: entitlement || null,
|
|
250
|
+
};
|
|
251
|
+
} catch (error) {
|
|
252
|
+
Logger.error(
|
|
253
|
+
'[RevenueCatAdapter] Failed to get subscription status: ' +
|
|
254
|
+
String(error),
|
|
255
|
+
);
|
|
256
|
+
return {
|
|
257
|
+
isActive: false,
|
|
258
|
+
details: { error: String(error) },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Restore purchases
|
|
265
|
+
*/
|
|
266
|
+
async restorePurchases(): Promise<{
|
|
267
|
+
success: boolean;
|
|
268
|
+
error?: string;
|
|
269
|
+
restoredEntitlements?: string[];
|
|
270
|
+
}> {
|
|
271
|
+
this.ensureInitialized();
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const customerInfo = await Purchases.restorePurchases();
|
|
275
|
+
Logger.info('[RevenueCatAdapter] Purchases restored');
|
|
276
|
+
|
|
277
|
+
const restoredEntitlements = Object.keys(
|
|
278
|
+
customerInfo.entitlements.active,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
success: true,
|
|
283
|
+
restoredEntitlements,
|
|
284
|
+
};
|
|
285
|
+
} catch (error) {
|
|
286
|
+
Logger.error(
|
|
287
|
+
'[RevenueCatAdapter] Failed to restore purchases: ' + String(error),
|
|
288
|
+
);
|
|
289
|
+
return {
|
|
290
|
+
success: false,
|
|
291
|
+
error: String(error),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Record consumable purchase (not typically used with RevenueCat subscriptions)
|
|
298
|
+
*/
|
|
299
|
+
async recordConsumablePurchase(_details: {
|
|
300
|
+
productId: string;
|
|
301
|
+
quantity: number;
|
|
302
|
+
transactionDetails: any;
|
|
303
|
+
}): Promise<{ success: boolean; error?: string }> {
|
|
304
|
+
Logger.warn(
|
|
305
|
+
'[RevenueCatAdapter] recordConsumablePurchase not implemented for RevenueCat',
|
|
306
|
+
);
|
|
307
|
+
return {
|
|
308
|
+
success: false,
|
|
309
|
+
error: 'Consumable purchases not supported by RevenueCat adapter',
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Set user ID for RevenueCat
|
|
315
|
+
*/
|
|
316
|
+
static async setUserId(userId: string) {
|
|
317
|
+
RevenueCatAdapter.ensureInitialized();
|
|
318
|
+
try {
|
|
319
|
+
const { customerInfo } = await Purchases.logIn(userId);
|
|
320
|
+
Logger.info(`[RevenueCatAdapter] User logged in: ${userId}`);
|
|
321
|
+
return customerInfo;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
Logger.error(
|
|
324
|
+
'[RevenueCatAdapter] Failed to set user ID: ' + String(error),
|
|
325
|
+
);
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Log out the current user
|
|
332
|
+
*/
|
|
333
|
+
static async logOut() {
|
|
334
|
+
RevenueCatAdapter.ensureInitialized();
|
|
335
|
+
try {
|
|
336
|
+
const customerInfo = await Purchases.logOut();
|
|
337
|
+
Logger.info('[RevenueCatAdapter] User logged out');
|
|
338
|
+
return customerInfo;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
Logger.error(
|
|
341
|
+
'[RevenueCatAdapter] Failed to log out user: ' + String(error),
|
|
342
|
+
);
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Ensure the SDK is initialized before making calls
|
|
349
|
+
* @private
|
|
350
|
+
*/
|
|
351
|
+
private ensureInitialized(): void {
|
|
352
|
+
if (!RevenueCatAdapter.isInitialized) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
'[RevenueCatAdapter] SDK not initialized. Call initialize() first.',
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Static version for class-level calls
|
|
361
|
+
* @private
|
|
362
|
+
*/
|
|
363
|
+
private static ensureInitialized(): void {
|
|
364
|
+
if (!RevenueCatAdapter.isInitialized) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
'[RevenueCatAdapter] SDK not initialized. Call initialize() first.',
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Check if a product ID represents a consumable purchase (credits)
|
|
373
|
+
* @private
|
|
374
|
+
*/
|
|
375
|
+
private isConsumableProduct(productId: string): boolean {
|
|
376
|
+
// Define patterns for consumable products (credits)
|
|
377
|
+
return productId.includes('credits') || productId.includes('credit');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Get the number of credits for a consumable product
|
|
382
|
+
* @private
|
|
383
|
+
*/
|
|
384
|
+
private getConsumableQuantity(productId: string): number {
|
|
385
|
+
// Extract quantity from product ID patterns
|
|
386
|
+
const patterns = [
|
|
387
|
+
/credits?[_-]?(\d+)/i, // credits_100, credit_50, credits100
|
|
388
|
+
/(\d+)[_-]?credits?/i, // 100_credits, 50credit, 100credits
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
for (const pattern of patterns) {
|
|
392
|
+
const match = productId.match(pattern);
|
|
393
|
+
if (match && match[1]) {
|
|
394
|
+
const quantity = Number.parseInt(match[1], 10);
|
|
395
|
+
if (!isNaN(quantity) && quantity > 0) {
|
|
396
|
+
return quantity;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Default to 1 credit if pattern not recognized
|
|
402
|
+
Logger.warn(
|
|
403
|
+
`[RevenueCatAdapter] Could not parse quantity from productId: ${productId}, defaulting to 1`,
|
|
404
|
+
);
|
|
405
|
+
return 1;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "payments",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "In-app purchases and subscriptions with RevenueCat",
|
|
5
|
+
"copy": [
|
|
6
|
+
{
|
|
7
|
+
"from": "apps/native/src/app/(root)/(protected)/paywall",
|
|
8
|
+
"to": "apps/native/src/app/(root)/(protected)/paywall"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"from": "apps/native/src/features/payments",
|
|
12
|
+
"to": "apps/native/src/features/payments"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"nav": {
|
|
16
|
+
"href": "/(root)/(protected)/paywall",
|
|
17
|
+
"label": "Paywall",
|
|
18
|
+
"icon": "💳",
|
|
19
|
+
"color": "#10B981"
|
|
20
|
+
},
|
|
21
|
+
"target": "native",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"expo": [
|
|
24
|
+
"react-native-purchases"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"manualSteps": [
|
|
28
|
+
{
|
|
29
|
+
"title": "Create RevenueCat Account",
|
|
30
|
+
"description": "Sign up at revenuecat.com and create a new project",
|
|
31
|
+
"link": "https://app.revenuecat.com/signup"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"title": "Configure App Stores",
|
|
35
|
+
"description": "Link your iOS (App Store Connect) and Android (Google Play) apps to RevenueCat",
|
|
36
|
+
"link": "https://www.revenuecat.com/docs/getting-started"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"title": "Get API Keys",
|
|
40
|
+
"description": "Copy your public API keys (iOS and Android) from RevenueCat settings",
|
|
41
|
+
"link": "https://app.revenuecat.com/settings/api-keys"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"title": "Update API Keys",
|
|
45
|
+
"description": "Replace the API keys in the RevenueCat adapter",
|
|
46
|
+
"file": "apps/native/src/features/payments/services/revenuecat-adapter.ts",
|
|
47
|
+
"content": "const REVENUECAT_API_KEY_IOS = 'your_ios_key_here';\nconst REVENUECAT_API_KEY_ANDROID = 'your_android_key_here';"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"title": "Create Products & Entitlements",
|
|
51
|
+
"description": "Set up your subscription products and entitlements in RevenueCat dashboard",
|
|
52
|
+
"link": "https://www.revenuecat.com/docs/entitlements"
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"postInstall": {
|
|
56
|
+
"message": "💳 Payments feature installed! Configure RevenueCat API keys to enable subscriptions."
|
|
57
|
+
}
|
|
58
|
+
}
|
|
Binary file
|