kasy-cli 1.16.0 → 1.18.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 +16 -2
- package/lib/commands/add.js +52 -19
- package/lib/commands/configure.js +548 -0
- package/lib/commands/deploy.js +4 -4
- package/lib/commands/doctor.js +54 -6
- package/lib/commands/favicon.js +4 -4
- package/lib/commands/icon.js +5 -5
- package/lib/commands/new.js +404 -213
- package/lib/commands/remove.js +14 -3
- package/lib/commands/run.js +208 -6
- package/lib/commands/splash.js +5 -5
- package/lib/commands/update.js +9 -9
- package/lib/scaffold/CHANGELOG.json +23 -0
- package/lib/scaffold/backends/api/patch/README.md +3 -2
- package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +108 -0
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +44 -5
- package/lib/scaffold/backends/supabase/patch/README.md +3 -2
- package/lib/scaffold/generate.js +24 -8
- package/lib/scaffold/shared/generator-utils.js +52 -8
- package/lib/scaffold/shared/post-build.js +113 -31
- package/lib/scaffold/shared/template-strings.js +6 -0
- package/lib/utils/brand.js +16 -12
- package/lib/utils/flutter-run.js +139 -11
- package/lib/utils/i18n/messages-en.js +85 -7
- package/lib/utils/i18n/messages-es.js +85 -7
- package/lib/utils/i18n/messages-pt.js +86 -8
- package/lib/utils/ui.js +79 -4
- package/package.json +1 -1
- package/templates/firebase/README.en.md +18 -8
- package/templates/firebase/README.es.md +18 -8
- package/templates/firebase/README.md +18 -8
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +68 -45
- 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/android/app/src/main/res/drawable/widget_add_button.xml +2 -2
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +7 -2
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +6 -6
- package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +1 -1
- package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +2 -2
- package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +1 -1
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +3 -3
- package/templates/firebase/android/app/src/main/res/values/colors.xml +32 -0
- package/templates/firebase/assets/images/splash_logo_dark.png +0 -0
- package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
- package/templates/firebase/assets/images/splash_logo_light.png +0 -0
- package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
- package/templates/firebase/docs/revenuecat-setup.es.md +28 -8
- package/templates/firebase/docs/revenuecat-setup.pt.md +28 -8
- package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +75 -29
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png +0 -0
- package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +1 -1
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_avatar.dart +65 -17
- package/templates/firebase/lib/components/kasy_avatar_presets.dart +121 -97
- package/templates/firebase/lib/components/kasy_button.dart +8 -8
- package/templates/firebase/lib/components/kasy_date_picker.dart +834 -0
- package/templates/firebase/lib/components/kasy_tabs.dart +145 -61
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +45 -53
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +565 -77
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +10 -5
- package/templates/firebase/lib/i18n/en.i18n.json +2 -1
- package/templates/firebase/lib/i18n/es.i18n.json +2 -1
- package/templates/firebase/lib/i18n/pt.i18n.json +2 -1
- package/templates/firebase/lib/router.dart +15 -1
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/web/index.html +9 -0
- package/templates/firebase/web/splash/img/dark-1x.png +0 -0
- package/templates/firebase/web/splash/img/dark-2x.png +0 -0
- package/templates/firebase/web/splash/img/dark-3x.png +0 -0
- package/templates/firebase/web/splash/img/dark-4x.png +0 -0
- package/templates/firebase/web/splash/img/light-1x.png +0 -0
- package/templates/firebase/web/splash/img/light-2x.png +0 -0
- package/templates/firebase/web/splash/img/light-3x.png +0 -0
- package/templates/firebase/web/splash/img/light-4x.png +0 -0
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,10 +320,21 @@ 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 {
|
|
135
|
-
await spawnFlutterWithSpinner(args, projectDir, t);
|
|
337
|
+
await spawnFlutterWithSpinner(args, projectDir, t, { raw: Boolean(options.raw) });
|
|
136
338
|
} catch (err) {
|
|
137
339
|
if (err.code === 'ENOENT') {
|
|
138
340
|
throw new Error(t('run.error.flutterNotFound'));
|
package/lib/commands/splash.js
CHANGED
|
@@ -5,7 +5,7 @@ const { exec } = require('node:child_process');
|
|
|
5
5
|
const { promisify } = require('node:util');
|
|
6
6
|
const kleur = require('kleur');
|
|
7
7
|
const ui = require('../utils/ui');
|
|
8
|
-
const { printCompactHeader } = require('../utils/brand');
|
|
8
|
+
const { printCompactHeader, paintLime } = require('../utils/brand');
|
|
9
9
|
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
10
10
|
const { writeAndroid12Variant } = require('../utils/png-padding');
|
|
11
11
|
|
|
@@ -117,7 +117,7 @@ async function runSplash(projectDir, options = {}) {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
const inspectSpinner = ui.spinner();
|
|
120
|
+
const inspectSpinner = ui.spinner({ color: paintLime });
|
|
121
121
|
inspectSpinner.start(t('splash.validating'));
|
|
122
122
|
|
|
123
123
|
let lightInfo;
|
|
@@ -165,13 +165,13 @@ async function runSplash(projectDir, options = {}) {
|
|
|
165
165
|
const destLightA12 = path.join(projectDir, ASSETS_DIR, LIGHT_ANDROID12_NAME);
|
|
166
166
|
const destDarkA12 = path.join(projectDir, ASSETS_DIR, DARK_ANDROID12_NAME);
|
|
167
167
|
|
|
168
|
-
const copySpinner = ui.spinner();
|
|
168
|
+
const copySpinner = ui.spinner({ color: paintLime });
|
|
169
169
|
copySpinner.start(t('splash.copying'));
|
|
170
170
|
await fs.copy(lightPath, destLight, { overwrite: true });
|
|
171
171
|
await fs.copy(darkPath, destDark, { overwrite: true });
|
|
172
172
|
copySpinner.stop(t('splash.copied'));
|
|
173
173
|
|
|
174
|
-
const a12Spinner = ui.spinner();
|
|
174
|
+
const a12Spinner = ui.spinner({ color: paintLime });
|
|
175
175
|
a12Spinner.start(t('splash.android12Generating'));
|
|
176
176
|
await writeAndroid12Variant(destLight, destLightA12);
|
|
177
177
|
await writeAndroid12Variant(destDark, destDarkA12);
|
|
@@ -183,7 +183,7 @@ async function runSplash(projectDir, options = {}) {
|
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
const genSpinner = ui.spinner();
|
|
186
|
+
const genSpinner = ui.spinner({ color: paintLime });
|
|
187
187
|
genSpinner.start(t('splash.generating'));
|
|
188
188
|
const result = await runFlutterNativeSplash(projectDir);
|
|
189
189
|
if (result.ok) {
|
package/lib/commands/update.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs = require('fs-extra');
|
|
|
7
7
|
const pkg = require('../../package.json');
|
|
8
8
|
const kleur = require('kleur');
|
|
9
9
|
const ui = require('../utils/ui');
|
|
10
|
-
const { printCompactHeader } = require('../utils/brand');
|
|
10
|
+
const { printCompactHeader, paintLime } = require('../utils/brand');
|
|
11
11
|
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
12
12
|
const {
|
|
13
13
|
AVAILABLE_FEATURES,
|
|
@@ -185,7 +185,7 @@ async function runUpdate(module, options = {}) {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
const spinner = ui.spinner();
|
|
188
|
+
const spinner = ui.spinner({ color: paintLime });
|
|
189
189
|
spinner.start(t('update.applyingComponents'));
|
|
190
190
|
try {
|
|
191
191
|
const filesApplied = await applyBaseComponents(projectDir);
|
|
@@ -201,7 +201,7 @@ async function runUpdate(module, options = {}) {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
{
|
|
204
|
-
const spinnerPubGet = ui.spinner();
|
|
204
|
+
const spinnerPubGet = ui.spinner({ color: paintLime });
|
|
205
205
|
spinnerPubGet.start(t('update.pubGet'));
|
|
206
206
|
try {
|
|
207
207
|
await execAsync('flutter pub get', { cwd: projectDir, timeout: 300_000 });
|
|
@@ -233,7 +233,7 @@ async function runUpdate(module, options = {}) {
|
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
const spinner = ui.spinner();
|
|
236
|
+
const spinner = ui.spinner({ color: paintLime });
|
|
237
237
|
spinner.start(t('update.applyingCore'));
|
|
238
238
|
try {
|
|
239
239
|
const filesApplied = await applyCoreFiles(projectDir);
|
|
@@ -249,7 +249,7 @@ async function runUpdate(module, options = {}) {
|
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
{
|
|
252
|
-
const spinnerPubGet = ui.spinner();
|
|
252
|
+
const spinnerPubGet = ui.spinner({ color: paintLime });
|
|
253
253
|
spinnerPubGet.start(t('update.pubGet'));
|
|
254
254
|
try {
|
|
255
255
|
await execAsync('flutter pub get', { cwd: projectDir, timeout: 300_000 });
|
|
@@ -284,7 +284,7 @@ async function runUpdate(module, options = {}) {
|
|
|
284
284
|
return;
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
|
-
const spinner = ui.spinner();
|
|
287
|
+
const spinner = ui.spinner({ color: paintLime });
|
|
288
288
|
spinner.start(t('update.applying', { module: IOS_RELEASE_UPDATE_TARGET }));
|
|
289
289
|
try {
|
|
290
290
|
const { tokens, pathReplacements } = buildTokens({
|
|
@@ -345,7 +345,7 @@ async function runUpdate(module, options = {}) {
|
|
|
345
345
|
|
|
346
346
|
// Re-apply patch
|
|
347
347
|
{
|
|
348
|
-
const spinner = ui.spinner();
|
|
348
|
+
const spinner = ui.spinner({ color: paintLime });
|
|
349
349
|
spinner.start(t('update.applying', { module: normalized }));
|
|
350
350
|
try {
|
|
351
351
|
const { tokens, pathReplacements } = buildTokens({
|
|
@@ -362,7 +362,7 @@ async function runUpdate(module, options = {}) {
|
|
|
362
362
|
|
|
363
363
|
// flutter pub get
|
|
364
364
|
{
|
|
365
|
-
const spinner = ui.spinner();
|
|
365
|
+
const spinner = ui.spinner({ color: paintLime });
|
|
366
366
|
spinner.start(t('update.pubGet'));
|
|
367
367
|
try {
|
|
368
368
|
await execAsync('flutter pub get', { cwd: projectDir, timeout: 300_000 });
|
|
@@ -374,7 +374,7 @@ async function runUpdate(module, options = {}) {
|
|
|
374
374
|
|
|
375
375
|
// build_runner (only for modules that generate code)
|
|
376
376
|
if (NEEDS_BUILD_RUNNER.includes(normalized)) {
|
|
377
|
-
const spinner = ui.timedSpinner();
|
|
377
|
+
const spinner = ui.timedSpinner({ color: paintLime });
|
|
378
378
|
spinner.start(t('update.buildRunner'));
|
|
379
379
|
try {
|
|
380
380
|
await execAsync(
|
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.18.0": {
|
|
3
|
+
"modules": {
|
|
4
|
+
"components": {
|
|
5
|
+
"pt": "Novo KasyDatePicker (seleção de data com modo intervalo). KasyTabs redesenhado seguindo o Figma — espaçamento corrigido, transição suave (fade + slide) ao trocar de aba. KasyAvatar refatorado com gradientes estruturados (mais fácil de customizar).",
|
|
6
|
+
"en": "New KasyDatePicker (date selection with range mode). KasyTabs redesigned to match Figma — fixed spacing, smooth fade + slide transition when switching tabs. KasyAvatar refactored with structured gradients (easier to customize).",
|
|
7
|
+
"es": "Nuevo KasyDatePicker (selección de fecha con modo rango). KasyTabs rediseñado siguiendo el Figma — espaciado corregido, transición suave (fade + slide) al cambiar de pestaña. KasyAvatar refactorizado con gradientes estructurados (más fácil de personalizar)."
|
|
8
|
+
},
|
|
9
|
+
"widget": {
|
|
10
|
+
"pt": "Autor da citação agora aparece junto da quote no widget. Locale carregado sincronamente — corrige caso em que o widget abria em inglês mesmo com o app em pt/es por causa de race condition. Cores do widget Android passaram a vir de res/values/colors.xml (branding consistente, fácil de trocar). Layout do widget responde ao tamanho real (pequeno/médio/grande) sem cortar conteúdo.",
|
|
11
|
+
"en": "Quote author now appears alongside the quote in the widget. Locale loaded synchronously — fixes case where the widget opened in English even with the app in pt/es due to a race condition. Android widget colors now come from res/values/colors.xml (consistent branding, easy to swap). Widget layout responds to actual size (small/medium/large) without clipping content.",
|
|
12
|
+
"es": "El autor de la cita ahora aparece junto a la quote en el widget. Locale cargado sincrónicamente — corrige el caso en que el widget abría en inglés aunque la app estuviera en pt/es por una race condition. Los colores del widget Android ahora vienen de res/values/colors.xml (branding consistente, fácil de cambiar). El layout del widget responde al tamaño real (pequeño/mediano/grande) sin cortar contenido."
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"1.17.0": {
|
|
17
|
+
"modules": {
|
|
18
|
+
"revenuecat": {
|
|
19
|
+
"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.",
|
|
20
|
+
"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.",
|
|
21
|
+
"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."
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
2
25
|
"1.13.0": {
|
|
3
26
|
"modules": {
|
|
4
27
|
"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
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enable Firebase Auth providers using the official Firebase CLI flow:
|
|
3
|
+
*
|
|
4
|
+
* 1. Merge `firebase.json` with `auth.providers.{anonymous, emailPassword, googleSignIn}`
|
|
5
|
+
* 2. Run `firebase deploy --only auth --project <id>`
|
|
6
|
+
*
|
|
7
|
+
* Docs: https://firebase.google.com/docs/auth/configure-providers-cli
|
|
8
|
+
*
|
|
9
|
+
* This is the only documented way to activate Google Sign-In without manually
|
|
10
|
+
* clicking "Enable" in the Firebase Console: the deploy creates the OAuth 2.0
|
|
11
|
+
* Web Client automatically (same backend the Console hits internally).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const { promisify } = require('node:util');
|
|
16
|
+
const execAsync = promisify(require('node:child_process').exec);
|
|
17
|
+
const fs = require('fs-extra');
|
|
18
|
+
|
|
19
|
+
const DEPLOY_TIMEOUT_MS = 5 * 60 * 1000; // 5 min — auth deploys are fast but allow slack
|
|
20
|
+
|
|
21
|
+
async function getGcloudAccountEmail() {
|
|
22
|
+
try {
|
|
23
|
+
const { stdout } = await execAsync('gcloud config get-value account 2>/dev/null');
|
|
24
|
+
const email = (stdout || '').trim();
|
|
25
|
+
return email && email !== '(unset)' ? email : null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Merge auth.providers into the project's firebase.json without touching
|
|
33
|
+
* other top-level keys (functions, firestore, storage, flutter, etc.).
|
|
34
|
+
*/
|
|
35
|
+
async function mergeAuthIntoFirebaseJson(projectDir, providers) {
|
|
36
|
+
const firebaseJsonPath = path.join(projectDir, 'firebase.json');
|
|
37
|
+
if (!(await fs.pathExists(firebaseJsonPath))) {
|
|
38
|
+
return { ok: false, error: 'firebase.json not found in project root' };
|
|
39
|
+
}
|
|
40
|
+
let config;
|
|
41
|
+
try {
|
|
42
|
+
config = await fs.readJson(firebaseJsonPath);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return { ok: false, error: `Failed to parse firebase.json: ${err.message}` };
|
|
45
|
+
}
|
|
46
|
+
config.auth = {
|
|
47
|
+
...(config.auth || {}),
|
|
48
|
+
providers: {
|
|
49
|
+
...(config.auth?.providers || {}),
|
|
50
|
+
...providers,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
await fs.writeJson(firebaseJsonPath, config, { spaces: 2 });
|
|
54
|
+
return { ok: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {object} options
|
|
59
|
+
* @param {string} options.projectDir
|
|
60
|
+
* @param {string} options.projectId
|
|
61
|
+
* @param {string} options.appName
|
|
62
|
+
* @param {string} [options.supportEmail] - falls back to active gcloud account
|
|
63
|
+
* @returns {{ ok: boolean, error?: string, supportEmail?: string }}
|
|
64
|
+
*/
|
|
65
|
+
async function enableAuthViaFirebaseCli({ projectDir, projectId, appName, supportEmail }) {
|
|
66
|
+
// 1. Resolve support email (required by Google's OAuth consent screen)
|
|
67
|
+
let email = (supportEmail || '').trim();
|
|
68
|
+
if (!email) email = await getGcloudAccountEmail();
|
|
69
|
+
if (!email) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: 'support_email_required',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 2. Merge firebase.json
|
|
77
|
+
const merge = await mergeAuthIntoFirebaseJson(projectDir, {
|
|
78
|
+
anonymous: true,
|
|
79
|
+
emailPassword: true,
|
|
80
|
+
googleSignIn: {
|
|
81
|
+
oAuthBrandDisplayName: appName,
|
|
82
|
+
supportEmail: email,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
if (!merge.ok) return merge;
|
|
86
|
+
|
|
87
|
+
// 3. Deploy. --non-interactive prevents the CLI from prompting on edge cases.
|
|
88
|
+
const cmd = `firebase deploy --only auth --project ${projectId} --non-interactive`;
|
|
89
|
+
try {
|
|
90
|
+
await execAsync(cmd, { cwd: projectDir, timeout: DEPLOY_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 });
|
|
91
|
+
return { ok: true, supportEmail: email };
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const stderr = (err.stderr || '').toString();
|
|
94
|
+
const stdout = (err.stdout || '').toString();
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
error: (stderr || err.message || '').slice(0, 800),
|
|
98
|
+
stdout: stdout.slice(0, 800),
|
|
99
|
+
supportEmail: email,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
enableAuthViaFirebaseCli,
|
|
106
|
+
getGcloudAccountEmail,
|
|
107
|
+
mergeAuthIntoFirebaseJson,
|
|
108
|
+
};
|
|
@@ -689,6 +689,8 @@ async function enableAuthProviders(projectId, { maxRetries = 3, retryDelayMs = 1
|
|
|
689
689
|
enabled: true,
|
|
690
690
|
}),
|
|
691
691
|
});
|
|
692
|
+
let googleEnabled = false;
|
|
693
|
+
let googleSignInSkipped = false;
|
|
692
694
|
if (!googleRes.ok) {
|
|
693
695
|
const googleText = await googleRes.text();
|
|
694
696
|
// 409 = already exists — update it to ensure it's enabled.
|
|
@@ -703,13 +705,49 @@ async function enableAuthProviders(projectId, { maxRetries = 3, retryDelayMs = 1
|
|
|
703
705
|
},
|
|
704
706
|
body: JSON.stringify({ enabled: true }),
|
|
705
707
|
});
|
|
706
|
-
|
|
708
|
+
googleEnabled = true;
|
|
709
|
+
} else {
|
|
710
|
+
// 400 INVALID_CONFIG (client_id empty) = no OAuth client created yet.
|
|
711
|
+
// Email/Password and Anonymous are already enabled — just mark Google as skipped.
|
|
712
|
+
googleSignInSkipped = true;
|
|
707
713
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
return { ok: true, googleSignInSkipped: true };
|
|
714
|
+
} else {
|
|
715
|
+
googleEnabled = true;
|
|
711
716
|
}
|
|
712
|
-
|
|
717
|
+
|
|
718
|
+
// Step 4: Apple Sign-In (best effort). Apple requires Service ID + JWT client secret
|
|
719
|
+
// that we cannot generate without the user's Apple Developer credentials, but the
|
|
720
|
+
// provider entry itself can be created so the Console shows the row ready for the
|
|
721
|
+
// user to fill in when they ship to iOS. Failure here is silent.
|
|
722
|
+
const appleUrl = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/defaultSupportedIdpConfigs?idpId=apple.com`;
|
|
723
|
+
let appleEnabled = false;
|
|
724
|
+
try {
|
|
725
|
+
const appleRes = await fetch(appleUrl, {
|
|
726
|
+
method: 'POST',
|
|
727
|
+
headers: {
|
|
728
|
+
Authorization: `Bearer ${token}`,
|
|
729
|
+
'Content-Type': 'application/json',
|
|
730
|
+
'X-Goog-User-Project': projectId,
|
|
731
|
+
},
|
|
732
|
+
body: JSON.stringify({
|
|
733
|
+
name: `projects/${projectId}/defaultSupportedIdpConfigs/apple.com`,
|
|
734
|
+
enabled: true,
|
|
735
|
+
}),
|
|
736
|
+
});
|
|
737
|
+
if (appleRes.ok) {
|
|
738
|
+
appleEnabled = true;
|
|
739
|
+
} else {
|
|
740
|
+
const appleText = await appleRes.text();
|
|
741
|
+
if (appleRes.status === 409 || appleText.includes('ALREADY_EXISTS')) {
|
|
742
|
+
appleEnabled = true;
|
|
743
|
+
}
|
|
744
|
+
// Any other failure is non-fatal — Apple is a nice-to-have.
|
|
745
|
+
}
|
|
746
|
+
} catch (_) {
|
|
747
|
+
// Network error — skip silently.
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return { ok: true, googleSignInSkipped, googleEnabled, appleEnabled };
|
|
713
751
|
}
|
|
714
752
|
const text = await res.text();
|
|
715
753
|
lastError = `${res.status}: ${text}`;
|
|
@@ -766,6 +804,7 @@ async function setupFromScratch(appName, bundleId, options = {}) {
|
|
|
766
804
|
error: `Project created but billing link failed: ${shortError}. Manage projects: ${manageLink}`,
|
|
767
805
|
projectId,
|
|
768
806
|
billingFailed: true,
|
|
807
|
+
billingQuotaError: isQuota,
|
|
769
808
|
billingManualLink: manageLink,
|
|
770
809
|
};
|
|
771
810
|
}
|
|
@@ -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
|
|