kasy-cli 1.16.0 → 1.17.0
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/bin/kasy.js +1 -0
- package/lib/commands/add.js +45 -12
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/new.js +34 -8
- package/lib/commands/remove.js +14 -3
- package/lib/commands/run.js +207 -5
- package/lib/scaffold/CHANGELOG.json +9 -0
- package/lib/scaffold/backends/api/patch/README.md +3 -2
- package/lib/scaffold/backends/supabase/patch/README.md +3 -2
- package/lib/scaffold/shared/generator-utils.js +52 -8
- package/lib/scaffold/shared/post-build.js +105 -31
- package/lib/scaffold/shared/template-strings.js +6 -0
- package/lib/utils/i18n/messages-en.js +27 -2
- package/lib/utils/i18n/messages-es.js +27 -2
- package/lib/utils/i18n/messages-pt.js +27 -2
- package/package.json +1 -1
- package/templates/firebase/README.en.md +17 -7
- package/templates/firebase/README.es.md +17 -7
- package/templates/firebase/README.md +17 -7
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +15 -0
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +3 -19
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidgetReceiver.kt +37 -0
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/OpenAppAction.kt +26 -0
- package/templates/firebase/docs/revenuecat-setup.es.md +28 -8
- package/templates/firebase/docs/revenuecat-setup.pt.md +28 -8
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +39 -41
- package/templates/firebase/lib/router.dart +15 -1
package/bin/kasy.js
CHANGED
|
@@ -346,6 +346,7 @@ function buildProgram(language) {
|
|
|
346
346
|
.option('-d, --device <id>', 'Run on specific device ID')
|
|
347
347
|
.option('--prod', 'Use production dart-defines (from launch.json)')
|
|
348
348
|
.option('--no-defines', 'Skip dart-defines from launch.json')
|
|
349
|
+
.option('--rc <mode>', 'RevenueCat key mode: auto (default — picks by device), test, or prod')
|
|
349
350
|
.description(t('cli.command.run.description'))
|
|
350
351
|
.action(async (directory, options) => {
|
|
351
352
|
await runRun(directory, { language, ...options });
|
package/lib/commands/add.js
CHANGED
|
@@ -24,7 +24,7 @@ function findBaseDisplayName(id) {
|
|
|
24
24
|
|
|
25
25
|
const KASY_AUDIENCE = process.env.KASY_INTERNAL === '1' ? 'internal' : 'public';
|
|
26
26
|
const { applyPatch } = require('../scaffold/engine');
|
|
27
|
-
const { writeRouter, writeNoOpAnalyticsApi, writeNoOpTrackingApi, writeNoOpAdminHomeWidgets, writeNoOpFeatureRequestRepository, writeMainDart, addPubspecDep, localizeReleaseDocs } = require('../scaffold/shared/generator-utils');
|
|
27
|
+
const { writeRouter, writeNoOpAnalyticsApi, writeNoOpTrackingApi, writeNoOpAdminHomeWidgets, writeNoOpFeatureRequestRepository, writeMainDart, addPubspecDep, localizeReleaseDocs, resolveDefaultRcKeys } = require('../scaffold/shared/generator-utils');
|
|
28
28
|
const { toPackageName, buildTokens } = require('../scaffold/backends/firebase/tokens');
|
|
29
29
|
|
|
30
30
|
const execAsync = promisify(exec);
|
|
@@ -217,6 +217,14 @@ function applyKitSetupFlag(config, module, answers) {
|
|
|
217
217
|
case 'revenuecat':
|
|
218
218
|
config.subscriptionModule = true;
|
|
219
219
|
if (answers.revenuecatWeb) config.revenuecatWeb = true;
|
|
220
|
+
// Track which RC keys the user configured so `kasy doctor` can warn
|
|
221
|
+
// about release readiness without re-reading .env. Booleans only — we
|
|
222
|
+
// never persist the key values themselves to kit_setup.json.
|
|
223
|
+
config.revenuecatKeys = {
|
|
224
|
+
test: !!(answers.rcTestKey && answers.rcTestKey.trim()),
|
|
225
|
+
iosProd: !!(answers.rcIosProdKey && answers.rcIosProdKey.trim()),
|
|
226
|
+
androidProd: !!(answers.rcAndroidProdKey && answers.rcAndroidProdKey.trim()),
|
|
227
|
+
};
|
|
220
228
|
break;
|
|
221
229
|
case 'onboarding':
|
|
222
230
|
config.withOnboarding = true;
|
|
@@ -263,14 +271,18 @@ const MODULE_META = {
|
|
|
263
271
|
pubspecDeps: { 'facebook_app_events': '^0.24.0', 'flutter_facebook_auth': '^7.1.5' },
|
|
264
272
|
},
|
|
265
273
|
revenuecat: {
|
|
266
|
-
promptKeys: ['
|
|
267
|
-
defineUpdates: (a) =>
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
274
|
+
promptKeys: ['rcTestKey', 'rcIosProdKey', 'rcAndroidProdKey'],
|
|
275
|
+
defineUpdates: (a) => {
|
|
276
|
+
const { android, ios } = resolveDefaultRcKeys(a);
|
|
277
|
+
return {
|
|
278
|
+
RC_ANDROID_API_KEY: android,
|
|
279
|
+
RC_IOS_API_KEY: ios,
|
|
280
|
+
};
|
|
281
|
+
},
|
|
271
282
|
envLines: (a) => [
|
|
272
|
-
`
|
|
273
|
-
`
|
|
283
|
+
`RC_TEST_KEY=${(a.rcTestKey || '').trim()}`,
|
|
284
|
+
`RC_IOS_PROD_KEY=${(a.rcIosProdKey || '').trim()}`,
|
|
285
|
+
`RC_ANDROID_PROD_KEY=${(a.rcAndroidProdKey || '').trim()}`,
|
|
274
286
|
],
|
|
275
287
|
featureFlag: null,
|
|
276
288
|
pubspecDeps: {},
|
|
@@ -337,12 +349,33 @@ const PROMPT_QUESTIONS = {
|
|
|
337
349
|
onCancel: cancel,
|
|
338
350
|
}),
|
|
339
351
|
// RevenueCat SDK keys ship inside the client binary — not real secrets, plain text input.
|
|
340
|
-
|
|
341
|
-
|
|
352
|
+
// Three optional keys: test_ (covers iOS+Android in simulator), appl_ (iOS prod), goog_ (Android prod).
|
|
353
|
+
// kasy run picks the right one based on the device at launch time.
|
|
354
|
+
rcTestKey: (t, cancel) => ui.text({
|
|
355
|
+
message: t('add.prompt.rcTestKey'),
|
|
356
|
+
validate: (v) => {
|
|
357
|
+
const s = (v || '').trim();
|
|
358
|
+
if (!s) return undefined;
|
|
359
|
+
return /^test_/.test(s) ? undefined : t('new.firebase.q.revenuecat.test.invalid');
|
|
360
|
+
},
|
|
361
|
+
onCancel: cancel,
|
|
362
|
+
}),
|
|
363
|
+
rcIosProdKey: (t, cancel) => ui.text({
|
|
364
|
+
message: t('add.prompt.rcIosProdKey'),
|
|
365
|
+
validate: (v) => {
|
|
366
|
+
const s = (v || '').trim();
|
|
367
|
+
if (!s) return undefined;
|
|
368
|
+
return /^appl_/.test(s) ? undefined : t('new.firebase.q.revenuecat.iosProd.invalid');
|
|
369
|
+
},
|
|
342
370
|
onCancel: cancel,
|
|
343
371
|
}),
|
|
344
|
-
|
|
345
|
-
message: t('add.prompt.
|
|
372
|
+
rcAndroidProdKey: (t, cancel) => ui.text({
|
|
373
|
+
message: t('add.prompt.rcAndroidProdKey'),
|
|
374
|
+
validate: (v) => {
|
|
375
|
+
const s = (v || '').trim();
|
|
376
|
+
if (!s) return undefined;
|
|
377
|
+
return /^goog_/.test(s) ? undefined : t('new.firebase.q.revenuecat.androidProd.invalid');
|
|
378
|
+
},
|
|
346
379
|
onCancel: cancel,
|
|
347
380
|
}),
|
|
348
381
|
llmProvider: (t, cancel) => ui.select({
|
package/lib/commands/doctor.js
CHANGED
|
@@ -175,14 +175,45 @@ async function runRevenueCatChecks(projectDir, t, config) {
|
|
|
175
175
|
const rc = await validateRevenueCat(projectDir, config);
|
|
176
176
|
if (rc.skipped) return;
|
|
177
177
|
|
|
178
|
-
if (
|
|
179
|
-
|
|
178
|
+
if (rc.legacy) {
|
|
179
|
+
// Project predates the test/prod split — show the old summary.
|
|
180
|
+
if (!rc.iosEmpty && !rc.androidEmpty) {
|
|
181
|
+
ui.log.success(t('doctor.revenuecat.keysOk'));
|
|
182
|
+
} else {
|
|
183
|
+
ui.log.warn(t('doctor.revenuecat.keysEmpty'));
|
|
184
|
+
}
|
|
185
|
+
if (rc.bothTest) {
|
|
186
|
+
ui.log.warn(t('doctor.revenuecat.keysTest'));
|
|
187
|
+
}
|
|
180
188
|
} else {
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
// New 3-key scheme. One line per key with granular status.
|
|
190
|
+
if (rc.testOk) {
|
|
191
|
+
ui.log.success(t('doctor.revenuecat.testKeyOk'));
|
|
192
|
+
} else if (rc.testBadPrefix) {
|
|
193
|
+
ui.log.warn(t('doctor.revenuecat.prefixMismatch', { key: 'RC_TEST_KEY', expected: 'test_' }));
|
|
194
|
+
} else {
|
|
195
|
+
ui.log.warn(t('doctor.revenuecat.testKeyMissing'));
|
|
196
|
+
}
|
|
183
197
|
|
|
184
|
-
|
|
185
|
-
|
|
198
|
+
if (rc.iosProdOk) {
|
|
199
|
+
ui.log.success(t('doctor.revenuecat.iosProdOk'));
|
|
200
|
+
} else if (rc.iosProdBadPrefix) {
|
|
201
|
+
ui.log.warn(t('doctor.revenuecat.prefixMismatch', { key: 'RC_IOS_PROD_KEY', expected: 'appl_' }));
|
|
202
|
+
} else {
|
|
203
|
+
ui.log.message(kleur.dim(`– ${t('doctor.revenuecat.iosProdMissing')}`));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (rc.androidProdOk) {
|
|
207
|
+
ui.log.success(t('doctor.revenuecat.androidProdOk'));
|
|
208
|
+
} else if (rc.androidProdBadPrefix) {
|
|
209
|
+
ui.log.warn(t('doctor.revenuecat.prefixMismatch', { key: 'RC_ANDROID_PROD_KEY', expected: 'goog_' }));
|
|
210
|
+
} else {
|
|
211
|
+
ui.log.message(kleur.dim(`– ${t('doctor.revenuecat.androidProdMissing')}`));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (rc.onlyTest) {
|
|
215
|
+
ui.log.warn(t('doctor.revenuecat.keysTest'));
|
|
216
|
+
}
|
|
186
217
|
}
|
|
187
218
|
|
|
188
219
|
if (rc.webhookUrl) {
|
package/lib/commands/new.js
CHANGED
|
@@ -1192,16 +1192,42 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1192
1192
|
const moduleAnswers = {};
|
|
1193
1193
|
|
|
1194
1194
|
if (modules.includes('revenuecat')) {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1195
|
+
// Three keys, all optional, but we require at least one. The kasy run
|
|
1196
|
+
// command picks the right key based on the device:
|
|
1197
|
+
// simulator/emulator → RC_TEST_KEY
|
|
1198
|
+
// physical iOS → RC_IOS_PROD_KEY (falls back to test if missing)
|
|
1199
|
+
// physical Android → RC_ANDROID_PROD_KEY (falls back to test if missing)
|
|
1200
|
+
moduleAnswers.rcTestKey = ((await ui.text({
|
|
1201
|
+
message: tr('new.firebase.q.revenuecat.test'),
|
|
1202
|
+
validate: (v) => {
|
|
1203
|
+
const s = (v || '').trim();
|
|
1204
|
+
if (!s) return undefined;
|
|
1205
|
+
return /^test_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.test.invalid');
|
|
1206
|
+
},
|
|
1198
1207
|
onCancel: cancel,
|
|
1199
|
-
});
|
|
1200
|
-
moduleAnswers.
|
|
1201
|
-
message: tr('new.firebase.q.revenuecat.
|
|
1202
|
-
validate: (v) =>
|
|
1208
|
+
})) || '').trim();
|
|
1209
|
+
moduleAnswers.rcIosProdKey = ((await ui.text({
|
|
1210
|
+
message: tr('new.firebase.q.revenuecat.iosProd'),
|
|
1211
|
+
validate: (v) => {
|
|
1212
|
+
const s = (v || '').trim();
|
|
1213
|
+
if (!s) return undefined;
|
|
1214
|
+
return /^appl_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.iosProd.invalid');
|
|
1215
|
+
},
|
|
1203
1216
|
onCancel: cancel,
|
|
1204
|
-
});
|
|
1217
|
+
})) || '').trim();
|
|
1218
|
+
moduleAnswers.rcAndroidProdKey = ((await ui.text({
|
|
1219
|
+
message: tr('new.firebase.q.revenuecat.androidProd'),
|
|
1220
|
+
validate: (v) => {
|
|
1221
|
+
const s = (v || '').trim();
|
|
1222
|
+
if (!s) return undefined;
|
|
1223
|
+
return /^goog_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.androidProd.invalid');
|
|
1224
|
+
},
|
|
1225
|
+
onCancel: cancel,
|
|
1226
|
+
})) || '').trim();
|
|
1227
|
+
if (!moduleAnswers.rcTestKey && !moduleAnswers.rcIosProdKey && !moduleAnswers.rcAndroidProdKey) {
|
|
1228
|
+
// Non-blocking: user can fill .env later. Just warn so they're not surprised.
|
|
1229
|
+
ui.log.warn(tr('new.firebase.q.revenuecat.atLeastOne'));
|
|
1230
|
+
}
|
|
1205
1231
|
moduleAnswers.defaultPaywall = await ui.select({
|
|
1206
1232
|
message: tr('new.firebase.q.paywall'),
|
|
1207
1233
|
initialValue: 'basic',
|
package/lib/commands/remove.js
CHANGED
|
@@ -49,17 +49,28 @@ const MODULE_DEPS = {
|
|
|
49
49
|
const MODULE_DEFINES = {
|
|
50
50
|
sentry: ['SENTRY_DSN'],
|
|
51
51
|
analytics: ['MIXPANEL_TOKEN'],
|
|
52
|
-
revenuecat: ['RC_ANDROID_API_KEY', 'RC_IOS_API_KEY'],
|
|
52
|
+
revenuecat: ['RC_ANDROID_API_KEY', 'RC_IOS_API_KEY', 'RC_WEB_API_KEY'],
|
|
53
53
|
llm_chat: ['LLM_CHAT_ENDPOINT'],
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
* .env.example variable names to remove per module.
|
|
57
|
+
* .env.example variable names to remove per module. We list both the new
|
|
58
|
+
* test/prod split (RC_TEST_KEY / RC_IOS_PROD_KEY / RC_ANDROID_PROD_KEY) and
|
|
59
|
+
* the legacy single-key vars so removal works on any project age.
|
|
58
60
|
*/
|
|
59
61
|
const MODULE_ENV_KEYS = {
|
|
60
62
|
sentry: ['SENTRY_DSN'],
|
|
61
63
|
analytics: ['MIXPANEL_TOKEN'],
|
|
62
|
-
revenuecat: [
|
|
64
|
+
revenuecat: [
|
|
65
|
+
'RC_TEST_KEY',
|
|
66
|
+
'RC_IOS_PROD_KEY',
|
|
67
|
+
'RC_ANDROID_PROD_KEY',
|
|
68
|
+
'RC_WEB_API_KEY',
|
|
69
|
+
// Legacy single-key format — only present in projects generated before
|
|
70
|
+
// the test/prod split. Listed here so removal cleans them up too.
|
|
71
|
+
'RC_ANDROID_API_KEY',
|
|
72
|
+
'RC_IOS_API_KEY',
|
|
73
|
+
],
|
|
63
74
|
};
|
|
64
75
|
|
|
65
76
|
/**
|
package/lib/commands/run.js
CHANGED
|
@@ -69,6 +69,174 @@ async function readLaunchArgs(projectDir, useProd) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Parse the project `.env` into a plain object.
|
|
74
|
+
* Supports KEY=VALUE lines; ignores comments and blank lines.
|
|
75
|
+
* Quotes around values are stripped. Missing file returns {}.
|
|
76
|
+
*/
|
|
77
|
+
async function loadEnvFile(projectDir) {
|
|
78
|
+
const envPath = path.join(projectDir, '.env');
|
|
79
|
+
if (!(await fs.pathExists(envPath))) return {};
|
|
80
|
+
const content = await fs.readFile(envPath, 'utf8');
|
|
81
|
+
const env = {};
|
|
82
|
+
for (const rawLine of content.split('\n')) {
|
|
83
|
+
const line = rawLine.trim();
|
|
84
|
+
if (!line || line.startsWith('#')) continue;
|
|
85
|
+
const eq = line.indexOf('=');
|
|
86
|
+
if (eq === -1) continue;
|
|
87
|
+
const key = line.slice(0, eq).trim();
|
|
88
|
+
let value = line.slice(eq + 1).trim();
|
|
89
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
90
|
+
value = value.slice(1, -1);
|
|
91
|
+
}
|
|
92
|
+
if (key) env[key] = value;
|
|
93
|
+
}
|
|
94
|
+
return env;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Decide whether the chosen device is a physical phone or a simulator/emulator.
|
|
99
|
+
* Returns one of: 'ios-simulator' | 'ios-device' | 'android-emulator' |
|
|
100
|
+
* 'android-device' | 'web' | 'desktop' | 'unknown'.
|
|
101
|
+
*
|
|
102
|
+
* When the user passes a generic `--ios`/`--android` shortcut we list devices
|
|
103
|
+
* and use the only matching one. With ambiguous matches we return
|
|
104
|
+
* 'ios-unknown' / 'android-unknown' so the caller can pick a safe default
|
|
105
|
+
* (test key) and warn the user.
|
|
106
|
+
*/
|
|
107
|
+
function classifyTargetDevice(projectDir, options, pickedDevice) {
|
|
108
|
+
if (options.web) return 'web';
|
|
109
|
+
if (pickedDevice) return classifyDevice(pickedDevice);
|
|
110
|
+
|
|
111
|
+
const devices = listFlutterDevices(projectDir);
|
|
112
|
+
|
|
113
|
+
if (options.device) {
|
|
114
|
+
const match = devices.find((d) => d.id === options.device || d.name === options.device);
|
|
115
|
+
return match ? classifyDevice(match) : 'unknown';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.ios) {
|
|
119
|
+
const ios = devices.filter((d) => (d.targetPlatform || '').toLowerCase() === 'ios');
|
|
120
|
+
if (ios.length === 1) return classifyDevice(ios[0]);
|
|
121
|
+
return 'ios-unknown';
|
|
122
|
+
}
|
|
123
|
+
if (options.android) {
|
|
124
|
+
const android = devices.filter((d) => (d.targetPlatform || '').toLowerCase().startsWith('android'));
|
|
125
|
+
if (android.length === 1) return classifyDevice(android[0]);
|
|
126
|
+
return 'android-unknown';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// No flag, no picked device: flutter run picks the only device available.
|
|
130
|
+
// Use it to choose the right RC key when there's exactly one mobile device.
|
|
131
|
+
const mobile = devices.filter((d) => {
|
|
132
|
+
const p = (d.targetPlatform || '').toLowerCase();
|
|
133
|
+
return p === 'ios' || p.startsWith('android');
|
|
134
|
+
});
|
|
135
|
+
if (mobile.length === 1) return classifyDevice(mobile[0]);
|
|
136
|
+
|
|
137
|
+
return 'unknown';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve which RC keys to inject given the .env contents, the classified
|
|
142
|
+
* device, and the requested mode ('auto' | 'test' | 'prod').
|
|
143
|
+
*
|
|
144
|
+
* Returns { android, ios, mode, warnings } where `mode` is 'test' or 'prod'
|
|
145
|
+
* (informational) and `warnings` is an array of human-readable strings.
|
|
146
|
+
*
|
|
147
|
+
* Behavior:
|
|
148
|
+
* - 'test' mode → both keys = RC_TEST_KEY (or empty if missing).
|
|
149
|
+
* - 'prod' mode → ios = RC_IOS_PROD_KEY, android = RC_ANDROID_PROD_KEY.
|
|
150
|
+
* If either is missing we warn and fall back to RC_TEST_KEY for that one.
|
|
151
|
+
* - 'auto' mode → infer from device class:
|
|
152
|
+
* simulator/emulator/unknown → test
|
|
153
|
+
* ios-device → prod for iOS define (fallback test), test for Android
|
|
154
|
+
* android-device → prod for Android define (fallback test), test for iOS
|
|
155
|
+
*
|
|
156
|
+
* Legacy fallback: if .env has no RC_TEST_KEY / RC_*_PROD_KEY but has the
|
|
157
|
+
* old RC_ANDROID_API_KEY / RC_IOS_API_KEY, we honor those as-is so projects
|
|
158
|
+
* generated before the test/prod split keep working without manual migration.
|
|
159
|
+
*/
|
|
160
|
+
function resolveRcKeys(env, deviceClass, mode, t) {
|
|
161
|
+
const warnings = [];
|
|
162
|
+
const legacyAndroid = env.RC_ANDROID_API_KEY;
|
|
163
|
+
const legacyIos = env.RC_IOS_API_KEY;
|
|
164
|
+
const test = env.RC_TEST_KEY;
|
|
165
|
+
const iosProd = env.RC_IOS_PROD_KEY;
|
|
166
|
+
const androidProd = env.RC_ANDROID_PROD_KEY;
|
|
167
|
+
|
|
168
|
+
const hasNewKeys = !!(test || iosProd || androidProd);
|
|
169
|
+
if (!hasNewKeys && (legacyAndroid || legacyIos)) {
|
|
170
|
+
return {
|
|
171
|
+
android: legacyAndroid || '',
|
|
172
|
+
ios: legacyIos || '',
|
|
173
|
+
mode: 'legacy',
|
|
174
|
+
warnings,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const isSimulator = deviceClass === 'ios-simulator' || deviceClass === 'android-emulator';
|
|
179
|
+
const isIosDevice = deviceClass === 'ios-device';
|
|
180
|
+
const isAndroidDevice = deviceClass === 'android-device';
|
|
181
|
+
|
|
182
|
+
// Resolve effective mode.
|
|
183
|
+
let effectiveMode = mode;
|
|
184
|
+
if (mode === 'auto') {
|
|
185
|
+
effectiveMode = (isIosDevice || isAndroidDevice) ? 'prod' : 'test';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Build android+ios.
|
|
189
|
+
let android;
|
|
190
|
+
let ios;
|
|
191
|
+
|
|
192
|
+
if (effectiveMode === 'test') {
|
|
193
|
+
android = test || '';
|
|
194
|
+
ios = test || '';
|
|
195
|
+
} else {
|
|
196
|
+
// prod mode — pick per platform, fall back to test if prod missing.
|
|
197
|
+
if (isIosDevice) {
|
|
198
|
+
ios = iosProd || test || '';
|
|
199
|
+
android = test || '';
|
|
200
|
+
if (!iosProd) {
|
|
201
|
+
warnings.push(t('run.rc.fallbackToTest', { platform: 'iOS', var: 'IOS_PROD_KEY' }));
|
|
202
|
+
}
|
|
203
|
+
} else if (isAndroidDevice) {
|
|
204
|
+
android = androidProd || test || '';
|
|
205
|
+
ios = test || '';
|
|
206
|
+
if (!androidProd) {
|
|
207
|
+
warnings.push(t('run.rc.fallbackToTest', { platform: 'Android', var: 'ANDROID_PROD_KEY' }));
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
// Forced --rc=prod with no clear device: try both prod keys, fall back to test.
|
|
211
|
+
ios = iosProd || test || '';
|
|
212
|
+
android = androidProd || test || '';
|
|
213
|
+
if (!iosProd && !androidProd && !test) {
|
|
214
|
+
warnings.push(t('run.rc.forcedProdMissing'));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { android, ios, mode: effectiveMode, warnings };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Apply `replacements` to the dart-define list, replacing existing values
|
|
224
|
+
* for the same key. Returns a new array; never mutates the input.
|
|
225
|
+
*/
|
|
226
|
+
function overrideDartDefines(defines, replacements) {
|
|
227
|
+
const out = [];
|
|
228
|
+
const seen = new Set(Object.keys(replacements));
|
|
229
|
+
for (const def of defines) {
|
|
230
|
+
const m = def.match(/^--dart-define=([^=]+)=/);
|
|
231
|
+
if (m && seen.has(m[1])) continue;
|
|
232
|
+
out.push(def);
|
|
233
|
+
}
|
|
234
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
235
|
+
out.push(`--dart-define=${key}=${value}`);
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
72
240
|
async function runRun(directory, options = {}) {
|
|
73
241
|
const t = createTranslator(options.language || detectDefaultLanguage());
|
|
74
242
|
const projectDir = path.resolve(directory || '.');
|
|
@@ -82,6 +250,7 @@ async function runRun(directory, options = {}) {
|
|
|
82
250
|
// bails out with "More than one device connected" otherwise.
|
|
83
251
|
const deviceArgs = [];
|
|
84
252
|
let resolvedDeviceLabel = null;
|
|
253
|
+
let pickedDevice = null;
|
|
85
254
|
if (options.web) {
|
|
86
255
|
deviceArgs.push('-d', 'chrome');
|
|
87
256
|
} else if (options.ios) {
|
|
@@ -94,23 +263,45 @@ async function runRun(directory, options = {}) {
|
|
|
94
263
|
const devices = listFlutterDevices(projectDir);
|
|
95
264
|
if (devices.length > 1) {
|
|
96
265
|
printCompactHeader(t);
|
|
97
|
-
|
|
98
|
-
if (!
|
|
266
|
+
pickedDevice = await pickDevice(devices, t);
|
|
267
|
+
if (!pickedDevice) {
|
|
99
268
|
console.log(kleur.yellow(` ⚠ ${t('run.warn.nothingSelected')}`));
|
|
100
269
|
return;
|
|
101
270
|
}
|
|
102
|
-
deviceArgs.push('-d',
|
|
103
|
-
resolvedDeviceLabel = `${
|
|
271
|
+
deviceArgs.push('-d', pickedDevice.id);
|
|
272
|
+
resolvedDeviceLabel = `${pickedDevice.name} (${pickedDevice.id})`;
|
|
104
273
|
}
|
|
105
274
|
// 0 or 1 device → let flutter handle it; it picks the only one or
|
|
106
275
|
// prints its own "no devices" message.
|
|
107
276
|
}
|
|
108
277
|
|
|
109
278
|
// Read dart-defines from .vscode/launch.json (skip if --no-defines)
|
|
110
|
-
|
|
279
|
+
let dartDefines = options.noDefines
|
|
111
280
|
? []
|
|
112
281
|
: await readLaunchArgs(projectDir, options.prod);
|
|
113
282
|
|
|
283
|
+
// Override RC keys based on device. We only touch RC_ANDROID_API_KEY /
|
|
284
|
+
// RC_IOS_API_KEY when the launch.json already declares them — that's the
|
|
285
|
+
// signal the project uses RevenueCat. Web defines (RC_WEB_API_KEY) aren't
|
|
286
|
+
// touched here; they're already a single key.
|
|
287
|
+
const usesRc = dartDefines.some(
|
|
288
|
+
(a) => a.startsWith('--dart-define=RC_ANDROID_API_KEY=') ||
|
|
289
|
+
a.startsWith('--dart-define=RC_IOS_API_KEY='),
|
|
290
|
+
);
|
|
291
|
+
let rcInfo = null;
|
|
292
|
+
if (usesRc && !options.web) {
|
|
293
|
+
const env = await loadEnvFile(projectDir);
|
|
294
|
+
const mode = (options.rc || 'auto').toLowerCase();
|
|
295
|
+
const deviceClass = classifyTargetDevice(projectDir, options, pickedDevice);
|
|
296
|
+
rcInfo = resolveRcKeys(env, deviceClass, mode, t);
|
|
297
|
+
if (rcInfo.mode !== 'legacy') {
|
|
298
|
+
dartDefines = overrideDartDefines(dartDefines, {
|
|
299
|
+
RC_ANDROID_API_KEY: rcInfo.android,
|
|
300
|
+
RC_IOS_API_KEY: rcInfo.ios,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
114
305
|
const args = ['run', ...deviceArgs, ...dartDefines];
|
|
115
306
|
|
|
116
307
|
const envDefine = dartDefines.find((a) => a.startsWith('--dart-define=ENV='));
|
|
@@ -129,6 +320,17 @@ async function runRun(directory, options = {}) {
|
|
|
129
320
|
|
|
130
321
|
printCompactHeader(t);
|
|
131
322
|
console.log(kleur.bold(`${t('run.launching')}${summary}`));
|
|
323
|
+
if (rcInfo && rcInfo.mode !== 'legacy') {
|
|
324
|
+
const mode = (options.rc || 'auto').toLowerCase();
|
|
325
|
+
let label;
|
|
326
|
+
if (mode === 'test') label = t('run.rc.forcedTest');
|
|
327
|
+
else if (mode === 'prod') label = t('run.rc.forcedProd');
|
|
328
|
+
else label = rcInfo.mode === 'prod' ? t('run.rc.usingProd') : t('run.rc.usingTest');
|
|
329
|
+
console.log(kleur.dim(` ✦ ${label}`));
|
|
330
|
+
for (const w of rcInfo.warnings) {
|
|
331
|
+
console.log(kleur.yellow(` ⚠ ${w}`));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
132
334
|
console.log(kleur.dim(` ✦ ${t('run.updateHint.prefix')} `) + kleur.cyan('kasy update') + kleur.dim(` ${t('run.updateHint.suffix')}\n`));
|
|
133
335
|
|
|
134
336
|
try {
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.17.0": {
|
|
3
|
+
"modules": {
|
|
4
|
+
"revenuecat": {
|
|
5
|
+
"pt": "kasy run agora detecta se você está rodando em simulador/emulador ou dispositivo físico e injeta a chave certa automaticamente. Três chaves no .env: RC_TEST_KEY (test_, simulador), RC_IOS_PROD_KEY (appl_, iPhone físico), RC_ANDROID_PROD_KEY (goog_, Android físico). Projetos antigos continuam funcionando — pra ganhar a separação, basta adicionar as 3 chaves no .env. Forçar manualmente: kasy run --rc=test ou --rc=prod.",
|
|
6
|
+
"en": "kasy run now detects whether you're on a simulator/emulator or a physical device and injects the right key automatically. Three keys in .env: RC_TEST_KEY (test_, simulator), RC_IOS_PROD_KEY (appl_, physical iPhone), RC_ANDROID_PROD_KEY (goog_, physical Android). Old projects keep working — to get the split, just add the 3 keys to .env. Force manually: kasy run --rc=test or --rc=prod.",
|
|
7
|
+
"es": "kasy run ahora detecta si estás corriendo en simulador/emulador o dispositivo físico e inyecta la clave correcta automáticamente. Tres claves en .env: RC_TEST_KEY (test_, simulador), RC_IOS_PROD_KEY (appl_, iPhone físico), RC_ANDROID_PROD_KEY (goog_, Android físico). Los proyectos antiguos siguen funcionando — para obtener el split, basta con agregar las 3 claves en .env. Forzar manualmente: kasy run --rc=test o --rc=prod."
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
2
11
|
"1.13.0": {
|
|
3
12
|
"modules": {
|
|
4
13
|
"onboarding": {
|
|
@@ -32,8 +32,9 @@ Ficam no `.vscode/launch.json` como variáveis de ambiente de build (`--dart-def
|
|
|
32
32
|
| Variável | Módulo | Como obter |
|
|
33
33
|
|----------|--------|------------|
|
|
34
34
|
| `BACKEND_URL` | API REST | URL da sua API |
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
35
|
+
| `RC_TEST_KEY` | RevenueCat | Dashboard RevenueCat → Apps → Test Store (chave `test_`, vale iOS+Android, usada automaticamente em simulador) |
|
|
36
|
+
| `RC_IOS_PROD_KEY` | RevenueCat | Dashboard RevenueCat → Apps → App Store (chave `appl_`, usada automaticamente em iPhone físico) |
|
|
37
|
+
| `RC_ANDROID_PROD_KEY` | RevenueCat | Dashboard RevenueCat → Apps → Google Play (chave `goog_`, usada automaticamente em Android físico) |
|
|
37
38
|
| `SENTRY_DSN` | Sentry | Dashboard Sentry → Projeto → DSN |
|
|
38
39
|
| `MIXPANEL_TOKEN` | Mixpanel | Dashboard Mixpanel → Configurações → Token |
|
|
39
40
|
|
|
@@ -68,8 +68,9 @@ Ficam no `.vscode/launch.json` como variáveis de ambiente de build (`--dart-def
|
|
|
68
68
|
|----------|--------|------------|
|
|
69
69
|
| `BACKEND_URL` | Supabase | Dashboard Supabase → Project Settings → API → Project URL |
|
|
70
70
|
| `SUPABASE_TOKEN` | Supabase | Dashboard Supabase → Project Settings → API → anon key |
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
71
|
+
| `RC_TEST_KEY` | RevenueCat | Dashboard RevenueCat → Apps → Test Store (chave `test_`, vale iOS+Android, usada automaticamente em simulador) |
|
|
72
|
+
| `RC_IOS_PROD_KEY` | RevenueCat | Dashboard RevenueCat → Apps → App Store (chave `appl_`, usada automaticamente em iPhone físico) |
|
|
73
|
+
| `RC_ANDROID_PROD_KEY` | RevenueCat | Dashboard RevenueCat → Apps → Google Play (chave `goog_`, usada automaticamente em Android físico) |
|
|
73
74
|
| `SENTRY_DSN` | Sentry | Dashboard Sentry → Projeto → DSN |
|
|
74
75
|
| `MIXPANEL_TOKEN` | Mixpanel | Dashboard Mixpanel → Configurações → Token |
|
|
75
76
|
|
|
@@ -13,6 +13,36 @@ const pkg = require('../../../package.json');
|
|
|
13
13
|
/** Backend IDs: firebase | supabase | api */
|
|
14
14
|
const BACKENDS = Object.freeze(['firebase', 'supabase', 'api']);
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the default RC keys to bake into launch.json / Makefile.
|
|
18
|
+
*
|
|
19
|
+
* `kasy run` overrides these at runtime based on the device picked (test_ for
|
|
20
|
+
* simulator/emulator, appl_/goog_ for physical). The values here are only used
|
|
21
|
+
* when the user runs Flutter directly (e.g. F5 in VS Code, `make run`) without
|
|
22
|
+
* going through `kasy run`. We default to test_ since that's what works in the
|
|
23
|
+
* iOS Simulator and Android Emulator — the common dev case.
|
|
24
|
+
*
|
|
25
|
+
* Legacy support: projects generated before this refactor only ask for a single
|
|
26
|
+
* Android/iOS key (`rcAndroidKey` / `rcIosKey`). When those are passed we honor
|
|
27
|
+
* them as-is.
|
|
28
|
+
*/
|
|
29
|
+
function resolveDefaultRcKeys(answers) {
|
|
30
|
+
// Legacy single-key format (pre-test/prod split): keep behavior unchanged.
|
|
31
|
+
if ((answers.rcAndroidKey || answers.rcIosKey) && !answers.rcTestKey && !answers.rcIosProdKey && !answers.rcAndroidProdKey) {
|
|
32
|
+
return {
|
|
33
|
+
android: answers.rcAndroidKey || 'YOUR_REVENUECAT_ANDROID_KEY',
|
|
34
|
+
ios: answers.rcIosKey || 'YOUR_REVENUECAT_IOS_KEY',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const test = (answers.rcTestKey || '').trim();
|
|
38
|
+
const iosProd = (answers.rcIosProdKey || '').trim();
|
|
39
|
+
const androidProd = (answers.rcAndroidProdKey || '').trim();
|
|
40
|
+
return {
|
|
41
|
+
android: test || androidProd || 'YOUR_REVENUECAT_ANDROID_KEY',
|
|
42
|
+
ios: test || iosProd || 'YOUR_REVENUECAT_IOS_KEY',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
16
46
|
/**
|
|
17
47
|
* Build dart-define args for dev and prod environments.
|
|
18
48
|
* Backend-specific vars: Firebase (none extra), Supabase (BACKEND_URL, SUPABASE_TOKEN), API (BACKEND_URL).
|
|
@@ -40,12 +70,11 @@ function buildDartDefines(backend, modules, answers) {
|
|
|
40
70
|
}
|
|
41
71
|
|
|
42
72
|
if (modules.includes('revenuecat')) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
dev.push(`--dart-define=
|
|
46
|
-
|
|
47
|
-
prod.push(`--dart-define=
|
|
48
|
-
prod.push(`--dart-define=RC_IOS_API_KEY=${iosKey}`);
|
|
73
|
+
const { android: androidDefault, ios: iosDefault } = resolveDefaultRcKeys(answers);
|
|
74
|
+
dev.push(`--dart-define=RC_ANDROID_API_KEY=${androidDefault}`);
|
|
75
|
+
dev.push(`--dart-define=RC_IOS_API_KEY=${iosDefault}`);
|
|
76
|
+
prod.push(`--dart-define=RC_ANDROID_API_KEY=${androidDefault}`);
|
|
77
|
+
prod.push(`--dart-define=RC_IOS_API_KEY=${iosDefault}`);
|
|
49
78
|
if (answers.revenuecatWeb) {
|
|
50
79
|
const webKey = answers.rcWebKey || 'YOUR_REVENUECAT_WEB_KEY';
|
|
51
80
|
dev.push(`--dart-define=RC_WEB_API_KEY=${webKey}`);
|
|
@@ -144,8 +173,11 @@ async function writeEnvExample(projectDir, modules, answers, language = 'en') {
|
|
|
144
173
|
if (modules.includes('revenuecat')) {
|
|
145
174
|
lines.push('');
|
|
146
175
|
lines.push(t.revenuecat);
|
|
147
|
-
lines.push(
|
|
148
|
-
lines.push(`
|
|
176
|
+
if (t.revenuecatTest) lines.push(t.revenuecatTest);
|
|
177
|
+
lines.push(`RC_TEST_KEY=${(answers.rcTestKey || '').trim()}`);
|
|
178
|
+
if (t.revenuecatProd) lines.push(t.revenuecatProd);
|
|
179
|
+
lines.push(`RC_IOS_PROD_KEY=${(answers.rcIosProdKey || '').trim()}`);
|
|
180
|
+
lines.push(`RC_ANDROID_PROD_KEY=${(answers.rcAndroidProdKey || '').trim()}`);
|
|
149
181
|
if (answers.revenuecatWeb) {
|
|
150
182
|
lines.push(`RC_WEB_API_KEY=${answers.rcWebKey || 'YOUR_REVENUECAT_WEB_KEY'}`);
|
|
151
183
|
}
|
|
@@ -468,6 +500,16 @@ const bool revenuecatWeb = ${revenuecatWeb};
|
|
|
468
500
|
async function writeKitSetup(projectDir, options) {
|
|
469
501
|
const { appName, bundleId, backend, modules = [], firebaseProjectId, supabaseUrl, supabaseAnonKey, moduleAnswers = {} } = options;
|
|
470
502
|
const revenuecatWeb = !!(modules.includes('revenuecat') && modules.includes('web') && moduleAnswers.revenuecatWeb);
|
|
503
|
+
// Booleans tracking which RC keys the user configured. We never persist key
|
|
504
|
+
// values to kit_setup.json — those live in .env (gitignored). These flags
|
|
505
|
+
// let `kasy doctor` warn about release readiness without re-reading .env.
|
|
506
|
+
const revenuecatKeys = modules.includes('revenuecat')
|
|
507
|
+
? {
|
|
508
|
+
test: !!(moduleAnswers.rcTestKey && moduleAnswers.rcTestKey.trim()),
|
|
509
|
+
iosProd: !!(moduleAnswers.rcIosProdKey && moduleAnswers.rcIosProdKey.trim()),
|
|
510
|
+
androidProd: !!(moduleAnswers.rcAndroidProdKey && moduleAnswers.rcAndroidProdKey.trim()),
|
|
511
|
+
}
|
|
512
|
+
: undefined;
|
|
471
513
|
const config = {
|
|
472
514
|
appName: appName || 'App',
|
|
473
515
|
bundleId: bundleId || 'com.example.app',
|
|
@@ -480,6 +522,7 @@ async function writeKitSetup(projectDir, options) {
|
|
|
480
522
|
storageProvider: backend === 'firebase' ? 'firebase' : backend === 'supabase' ? 'supabase' : 'api',
|
|
481
523
|
webCompat: modules.includes('web'),
|
|
482
524
|
revenuecatWeb,
|
|
525
|
+
...(revenuecatKeys ? { revenuecatKeys } : {}),
|
|
483
526
|
internationalization: true,
|
|
484
527
|
useSentry: modules.includes('sentry'),
|
|
485
528
|
withOnboarding: modules.includes('onboarding'),
|
|
@@ -1638,6 +1681,7 @@ async function localizeReleaseDocs(projectDir, language = 'en') {
|
|
|
1638
1681
|
module.exports = {
|
|
1639
1682
|
BACKENDS,
|
|
1640
1683
|
buildDartDefines,
|
|
1684
|
+
resolveDefaultRcKeys,
|
|
1641
1685
|
writeRouter,
|
|
1642
1686
|
writeVsCodeLaunch,
|
|
1643
1687
|
writeEnvExample,
|