vibefast-cli 0.4.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.
Files changed (89) hide show
  1. package/FEATURE-DEPENDENCY-SPEC.md +338 -0
  2. package/dist/__tests__/integration.test.d.ts +2 -0
  3. package/dist/__tests__/integration.test.d.ts.map +1 -0
  4. package/dist/__tests__/integration.test.js +219 -0
  5. package/dist/__tests__/integration.test.js.map +1 -0
  6. package/dist/__tests__/recipes.test.d.ts +2 -0
  7. package/dist/__tests__/recipes.test.d.ts.map +1 -0
  8. package/dist/__tests__/recipes.test.js +143 -0
  9. package/dist/__tests__/recipes.test.js.map +1 -0
  10. package/dist/commands/__tests__/init.test.d.ts +2 -0
  11. package/dist/commands/__tests__/init.test.d.ts.map +1 -0
  12. package/dist/commands/__tests__/init.test.js +95 -0
  13. package/dist/commands/__tests__/init.test.js.map +1 -0
  14. package/dist/commands/__tests__/platform.test.d.ts +2 -0
  15. package/dist/commands/__tests__/platform.test.d.ts.map +1 -0
  16. package/dist/commands/__tests__/platform.test.js +123 -0
  17. package/dist/commands/__tests__/platform.test.js.map +1 -0
  18. package/dist/commands/add.d.ts.map +1 -1
  19. package/dist/commands/add.js +4 -5
  20. package/dist/commands/add.js.map +1 -1
  21. package/dist/commands/init.d.ts.map +1 -1
  22. package/dist/commands/init.js +12 -12
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/platform.d.ts +3 -0
  25. package/dist/commands/platform.d.ts.map +1 -0
  26. package/dist/commands/platform.js +245 -0
  27. package/dist/commands/platform.js.map +1 -0
  28. package/dist/core/journal.d.ts.map +1 -1
  29. package/dist/core/journal.js +36 -19
  30. package/dist/core/journal.js.map +1 -1
  31. package/dist/core/recipes.d.ts.map +1 -1
  32. package/dist/core/recipes.js +8 -39
  33. package/dist/core/recipes.js.map +1 -1
  34. package/dist/index.js +2 -0
  35. package/dist/index.js.map +1 -1
  36. package/package.json +1 -1
  37. package/recipes/ios-widget/recipe.json +78 -0
  38. package/recipes/ios-widget/targets/widget/AppIntent.swift +46 -0
  39. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png +0 -0
  40. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png +0 -0
  41. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png +0 -0
  42. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png +0 -0
  43. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png +0 -0
  44. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png +0 -0
  45. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png +0 -0
  46. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png +0 -0
  47. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png +0 -0
  48. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png +0 -0
  49. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png +0 -0
  50. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png +0 -0
  51. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png +0 -0
  52. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png +0 -0
  53. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
  54. package/recipes/ios-widget/targets/widget/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png +0 -0
  55. package/recipes/ios-widget/targets/widget/CalorieTrackerWidget.swift +424 -0
  56. package/recipes/ios-widget/targets/widget/HabitTrackerWidget.swift +305 -0
  57. package/recipes/ios-widget/targets/widget/Info.plist +11 -0
  58. package/recipes/ios-widget/targets/widget/WidgetLiveActivity.swift +75 -0
  59. package/recipes/ios-widget/targets/widget/expo-target.config.js +10 -0
  60. package/recipes/ios-widget/targets/widget/generated.entitlements +5 -0
  61. package/recipes/ios-widget/targets/widget/index.swift +18 -0
  62. package/recipes/ios-widget/targets/widget/widgets.swift +96 -0
  63. package/recipes/ios-widget@latest.zip +0 -0
  64. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +74 -0
  65. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +25 -0
  66. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +23 -0
  67. package/recipes/payments/apps/native/src/features/payments/README.md +200 -0
  68. package/recipes/payments/apps/native/src/features/payments/app/local-paywall.tsx +194 -0
  69. package/recipes/payments/apps/native/src/features/payments/app/remote-paywall.tsx +79 -0
  70. package/recipes/payments/apps/native/src/features/payments/components/payment-initializer.tsx +95 -0
  71. package/recipes/payments/apps/native/src/features/payments/components/paywall-error-state.tsx +60 -0
  72. package/recipes/payments/apps/native/src/features/payments/components/paywall-local-mode.tsx +116 -0
  73. package/recipes/payments/apps/native/src/features/payments/components/paywall-product-card.tsx +133 -0
  74. package/recipes/payments/apps/native/src/features/payments/components/paywall-remote-mode.tsx +146 -0
  75. package/recipes/payments/apps/native/src/features/payments/hooks/use-entitlement.ts +63 -0
  76. package/recipes/payments/apps/native/src/features/payments/index.ts +8 -0
  77. package/recipes/payments/apps/native/src/features/payments/services/revenuecat-adapter.ts +407 -0
  78. package/recipes/payments/recipe.json +58 -0
  79. package/recipes/payments@latest.zip +0 -0
  80. package/src/__tests__/integration.test.ts +249 -0
  81. package/src/__tests__/recipes.test.ts +168 -0
  82. package/src/commands/__tests__/init.test.ts +112 -0
  83. package/src/commands/__tests__/platform.test.ts +141 -0
  84. package/src/commands/add.ts +4 -5
  85. package/src/commands/init.ts +14 -15
  86. package/src/commands/platform.ts +309 -0
  87. package/src/core/journal.ts +42 -25
  88. package/src/core/recipes.ts +8 -40
  89. package/src/index.ts +2 -0
@@ -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