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/utils/ui.js
CHANGED
|
@@ -91,8 +91,20 @@ function cancel(message) { clack.cancel(message); }
|
|
|
91
91
|
* Spinner integrated with Clack's vertical line (│).
|
|
92
92
|
* Returns: { start(msg), message(msg), stop(msg, code?) }
|
|
93
93
|
* code: 0 = success (✦), 1 = cancel (■), 2 = error (▲)
|
|
94
|
+
*
|
|
95
|
+
* Optional `color` paints every text passed through (start/message/stop) —
|
|
96
|
+
* pass `paintLime` (from utils/brand) for the Kasy brand color.
|
|
94
97
|
*/
|
|
95
|
-
function spinner(
|
|
98
|
+
function spinner({ color } = {}) {
|
|
99
|
+
const s = clack.spinner();
|
|
100
|
+
if (typeof color !== 'function') return s;
|
|
101
|
+
return {
|
|
102
|
+
start(msg) { s.start(msg != null ? color(msg) : msg); },
|
|
103
|
+
message(msg) { s.message(msg != null ? color(msg) : msg); },
|
|
104
|
+
stop(msg, code) { s.stop(msg != null ? color(msg) : msg, code); },
|
|
105
|
+
error(msg) { s.error(msg != null ? color(msg) : msg); },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
96
108
|
|
|
97
109
|
/**
|
|
98
110
|
* Spinner with an automatic elapsed-time suffix that ticks every second.
|
|
@@ -104,16 +116,18 @@ function spinner() { return clack.spinner(); }
|
|
|
104
116
|
* // ... 73 seconds later
|
|
105
117
|
* s.stop('Deploy done'); // "Deploy done [1m 13s]"
|
|
106
118
|
*/
|
|
107
|
-
function timedSpinner() {
|
|
119
|
+
function timedSpinner({ color } = {}) {
|
|
108
120
|
const s = clack.spinner();
|
|
121
|
+
const paint = typeof color === 'function' ? color : (t) => t;
|
|
109
122
|
let startTime = null;
|
|
110
123
|
let currentMessage = '';
|
|
111
124
|
let tick = null;
|
|
112
125
|
|
|
113
126
|
const render = (msg) => {
|
|
114
127
|
if (!msg) return '';
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
const painted = paint(msg);
|
|
129
|
+
if (!startTime) return painted;
|
|
130
|
+
return `${painted} ${kleur.dim(`[${formatElapsedSeconds(startTime)}]`)}`;
|
|
117
131
|
};
|
|
118
132
|
|
|
119
133
|
const stopTick = () => {
|
|
@@ -197,6 +211,66 @@ function makeTimedStepper() {
|
|
|
197
211
|
};
|
|
198
212
|
}
|
|
199
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Single-line stepper for Quick mode. Same API as makeTimedStepper, but every
|
|
216
|
+
* `.next(text)` mutates the message of a single underlying spinner instead of
|
|
217
|
+
* closing it and opening a new one — so the user sees one line that keeps
|
|
218
|
+
* updating ("Creating Firebase project…" → "Waiting for propagation…" → …)
|
|
219
|
+
* with a running total timer instead of a tall stack of step lines.
|
|
220
|
+
*
|
|
221
|
+
* Optional `color` paints the running text (lime in Kasy Quick mode). The
|
|
222
|
+
* final success/fail message keeps the caller-supplied formatting untouched.
|
|
223
|
+
*/
|
|
224
|
+
function makeQuickStepper({ color } = {}) {
|
|
225
|
+
const paint = typeof color === 'function' ? color : (t) => t;
|
|
226
|
+
const s = timedSpinner();
|
|
227
|
+
let started = false;
|
|
228
|
+
let currentMsg = '';
|
|
229
|
+
return {
|
|
230
|
+
next(text) {
|
|
231
|
+
currentMsg = text;
|
|
232
|
+
if (!started) {
|
|
233
|
+
s.start(paint(text));
|
|
234
|
+
started = true;
|
|
235
|
+
} else {
|
|
236
|
+
s.message(paint(text));
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
update(text) {
|
|
240
|
+
currentMsg = text;
|
|
241
|
+
if (started) s.message(paint(text));
|
|
242
|
+
},
|
|
243
|
+
succeed(text) {
|
|
244
|
+
if (started) {
|
|
245
|
+
s.stop(paint(text || currentMsg));
|
|
246
|
+
started = false;
|
|
247
|
+
currentMsg = '';
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
fail(text) {
|
|
251
|
+
if (started) {
|
|
252
|
+
s.error(text || currentMsg);
|
|
253
|
+
started = false;
|
|
254
|
+
currentMsg = '';
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
warn(text) {
|
|
258
|
+
if (started) {
|
|
259
|
+
s.stop(`⚠ ${text || currentMsg}`);
|
|
260
|
+
started = false;
|
|
261
|
+
currentMsg = '';
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
stop() {
|
|
265
|
+
if (started) {
|
|
266
|
+
s.stop(paint(currentMsg));
|
|
267
|
+
started = false;
|
|
268
|
+
currentMsg = '';
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
200
274
|
/**
|
|
201
275
|
* Multi-step spinner: each .next(text) succeeds the previous step
|
|
202
276
|
* with the previous message, then starts a new step with `text`.
|
|
@@ -281,6 +355,7 @@ module.exports = {
|
|
|
281
355
|
timedSpinner,
|
|
282
356
|
makeStepper,
|
|
283
357
|
makeTimedStepper,
|
|
358
|
+
makeQuickStepper,
|
|
284
359
|
taskLog,
|
|
285
360
|
progress,
|
|
286
361
|
log,
|
package/package.json
CHANGED
|
@@ -48,16 +48,26 @@ They live in `.vscode/launch.json` as build environment variables (`--dart-defin
|
|
|
48
48
|
|
|
49
49
|
| Variable | Module | How to get |
|
|
50
50
|
|----------|--------|------------|
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
51
|
+
| `RC_TEST_KEY` | RevenueCat | RevenueCat dashboard → Apps → Test Store → key (`test_…`). **One key for both iOS+Android.** Auto-used on simulator/emulator. |
|
|
52
|
+
| `RC_IOS_PROD_KEY` | RevenueCat | RevenueCat dashboard → Apps → App Store → key (`appl_…`). Auto-used on physical iPhone (Sandbox and Production). |
|
|
53
|
+
| `RC_ANDROID_PROD_KEY` | RevenueCat | RevenueCat dashboard → Apps → Google Play → key (`goog_…`). Auto-used on physical Android. |
|
|
53
54
|
| `RC_WEB_API_KEY` | RevenueCat Web | RevenueCat dashboard → Apps → Web Billing → **production** key (`rcb_…`, not `rcb_sb_`) for release builds |
|
|
54
55
|
|
|
55
|
-
### RevenueCat: `
|
|
56
|
+
### RevenueCat: `kasy run` picks the right key automatically
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
The CLI detects whether you're running on **simulator/emulator** or **physical device** and injects the right key:
|
|
59
|
+
|
|
60
|
+
| Where you run | Key used |
|
|
61
|
+
|---|---|
|
|
62
|
+
| iOS Simulator / Android Emulator | `RC_TEST_KEY` (test_) |
|
|
63
|
+
| Physical iPhone | `RC_IOS_PROD_KEY` (appl_) — falls back to `RC_TEST_KEY` if missing |
|
|
64
|
+
| Physical Android | `RC_ANDROID_PROD_KEY` (goog_) — falls back to `RC_TEST_KEY` if missing |
|
|
65
|
+
|
|
66
|
+
Force manually: `kasy run --rc=test` or `kasy run --rc=prod`. `--rc=auto` (default) applies the rule above.
|
|
67
|
+
|
|
68
|
+
- **Why the split?** Simulators can't run real in-app purchases against `appl_`/`goog_` — only RevenueCat Test Store works there. On physical devices, `appl_`/`goog_` covers both Sandbox and Production (the SDK detects the environment).
|
|
69
|
+
- **TestFlight and release:** use the production keys. **NEVER ship `test_` to the store** — the RevenueCat SDK crashes the app on release.
|
|
70
|
+
- **VS Code (F5 without kasy run):** `launch.json` defaults to `RC_TEST_KEY` (or production if test_ is missing). To switch manually, run via `kasy run`.
|
|
61
71
|
| `SENTRY_DSN` | Sentry | Dashboard Sentry → Project → DSN |
|
|
62
72
|
| `MIXPANEL_TOKEN` | Mixpanel | Dashboard Mixpanel → Settings → Token |
|
|
63
73
|
|
|
@@ -175,6 +185,6 @@ No Mac: [docs/codemagic-release.md](docs/codemagic-release.md)
|
|
|
175
185
|
|
|
176
186
|
## Security
|
|
177
187
|
|
|
178
|
-
`.gitignore` already excludes: `firebase_key.json`, `.env`, `.env.*`, `*.pem`, `*.keystore`, `.kasy/apple.env`, `.kasy/codemagic.env`.
|
|
188
|
+
`.gitignore` already excludes: `firebase_key.json`, `.env`, `.env.*`, `*.pem`, `*.keystore`, `.kasy/apple.env`, `.kasy/codemagic.env`, `.kasy/*.log`.
|
|
179
189
|
|
|
180
190
|
Never commit credentials to the repository.
|
|
@@ -48,16 +48,26 @@ Están en `.vscode/launch.json` como variables de entorno de build (`--dart-defi
|
|
|
48
48
|
|
|
49
49
|
| Variable | Módulo | Cómo obtener |
|
|
50
50
|
|----------|--------|--------------|
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
51
|
+
| `RC_TEST_KEY` | RevenueCat | Panel RevenueCat → Apps → Test Store → clave (`test_…`). **Una sola clave sirve para iOS+Android.** Usada automáticamente en simulador/emulador. |
|
|
52
|
+
| `RC_IOS_PROD_KEY` | RevenueCat | Panel RevenueCat → Apps → App Store → clave (`appl_…`). Usada automáticamente en iPhone físico (Sandbox y Producción). |
|
|
53
|
+
| `RC_ANDROID_PROD_KEY` | RevenueCat | Panel RevenueCat → Apps → Google Play → clave (`goog_…`). Usada automáticamente en Android físico. |
|
|
53
54
|
| `RC_WEB_API_KEY` | RevenueCat Web | Panel RevenueCat → Apps → Web Billing → clave **producción** (`rcb_…`, no `rcb_sb_`) en builds release |
|
|
54
55
|
|
|
55
|
-
### RevenueCat: `
|
|
56
|
+
### RevenueCat: `kasy run` elige la clave automáticamente
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
La CLI detecta si vas a correr en **simulador/emulador** o en **dispositivo físico** e inyecta la clave correcta:
|
|
59
|
+
|
|
60
|
+
| Dónde corres | Clave usada |
|
|
61
|
+
|---|---|
|
|
62
|
+
| iOS Simulator / Android Emulator | `RC_TEST_KEY` (test_) |
|
|
63
|
+
| iPhone físico | `RC_IOS_PROD_KEY` (appl_) — fallback `RC_TEST_KEY` si falta |
|
|
64
|
+
| Android físico | `RC_ANDROID_PROD_KEY` (goog_) — fallback `RC_TEST_KEY` si falta |
|
|
65
|
+
|
|
66
|
+
Forzar manualmente: `kasy run --rc=test` o `kasy run --rc=prod`. `--rc=auto` (por defecto) aplica la regla anterior.
|
|
67
|
+
|
|
68
|
+
- **¿Por qué el split?** Los simuladores no pueden hacer compras reales con claves `appl_`/`goog_` — solo funciona Test Store de RevenueCat. En dispositivo físico, `appl_`/`goog_` cubre Sandbox y Producción (el SDK lo detecta).
|
|
69
|
+
- **TestFlight y release:** usa las claves de producción. **NUNCA subas `test_` a la tienda** — el SDK de RevenueCat crashea el app en release.
|
|
70
|
+
- **VS Code (F5 sin kasy run):** `launch.json` usa `RC_TEST_KEY` por defecto (o producción si test_ falta). Para alternar manualmente, corre `kasy run`.
|
|
61
71
|
| `SENTRY_DSN` | Sentry | Dashboard Sentry → Proyecto → DSN |
|
|
62
72
|
| `MIXPANEL_TOKEN` | Mixpanel | Dashboard Mixpanel → Configuración → Token |
|
|
63
73
|
|
|
@@ -175,6 +185,6 @@ Sin Mac: [docs/codemagic-release.md](docs/codemagic-release.md)
|
|
|
175
185
|
|
|
176
186
|
## Seguridad
|
|
177
187
|
|
|
178
|
-
El `.gitignore` ya excluye: `firebase_key.json`, `.env`, `.env.*`, `*.pem`, `*.keystore`, `.kasy/apple.env`, `.kasy/codemagic.env`.
|
|
188
|
+
El `.gitignore` ya excluye: `firebase_key.json`, `.env`, `.env.*`, `*.pem`, `*.keystore`, `.kasy/apple.env`, `.kasy/codemagic.env`, `.kasy/*.log`.
|
|
179
189
|
|
|
180
190
|
Nunca subas credenciales al repositorio.
|
|
@@ -48,16 +48,26 @@ Ficam no `.vscode/launch.json` como variáveis de ambiente de build (`--dart-def
|
|
|
48
48
|
|
|
49
49
|
| Variável | Módulo | Como obter |
|
|
50
50
|
|----------|--------|------------|
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
51
|
+
| `RC_TEST_KEY` | RevenueCat | Dashboard RevenueCat → Apps → Test Store → chave (`test_…`). **Uma chave só, serve pra iOS+Android.** Usada automaticamente em simulador/emulador. |
|
|
52
|
+
| `RC_IOS_PROD_KEY` | RevenueCat | Dashboard RevenueCat → Apps → App Store → chave (`appl_…`). Usada automaticamente em iPhone físico (Sandbox e Produção). |
|
|
53
|
+
| `RC_ANDROID_PROD_KEY` | RevenueCat | Dashboard RevenueCat → Apps → Google Play → chave (`goog_…`). Usada automaticamente em Android físico. |
|
|
53
54
|
| `RC_WEB_API_KEY` | RevenueCat Web | Dashboard RevenueCat → Apps → Web Billing → chave **produção** (`rcb_…`, não `rcb_sb_`) em builds release |
|
|
54
55
|
|
|
55
|
-
### RevenueCat: `
|
|
56
|
+
### RevenueCat: `kasy run` escolhe a chave automaticamente
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
A CLI detecta se você vai rodar em **simulador/emulador** ou em **dispositivo físico** e injeta a chave certa:
|
|
59
|
+
|
|
60
|
+
| Onde você roda | Chave usada |
|
|
61
|
+
|---|---|
|
|
62
|
+
| iOS Simulator / Android Emulator | `RC_TEST_KEY` (test_) |
|
|
63
|
+
| iPhone físico | `RC_IOS_PROD_KEY` (appl_) — fallback `RC_TEST_KEY` se ausente |
|
|
64
|
+
| Android físico | `RC_ANDROID_PROD_KEY` (goog_) — fallback `RC_TEST_KEY` se ausente |
|
|
65
|
+
|
|
66
|
+
Forçar manualmente: `kasy run --rc=test` ou `kasy run --rc=prod`. `--rc=auto` (default) usa a regra acima.
|
|
67
|
+
|
|
68
|
+
- **Por que o split?** Simulador iOS e emulador Android não conseguem fazer in-app purchase real com chaves `appl_`/`goog_` — só funciona Test Store da RevenueCat. Já em físico, `appl_`/`goog_` cobre Sandbox e Produção (o SDK detecta sozinho).
|
|
69
|
+
- **TestFlight e release:** use as chaves de produção. **NUNCA suba `test_` para a loja** — o SDK do RevenueCat crasha o app no release.
|
|
70
|
+
- **VS Code (F5 sem kasy run):** o `launch.json` carrega `RC_TEST_KEY` como default (ou produção se test_ ausente). Pra alternar manualmente, rode pelo `kasy run`.
|
|
61
71
|
| `SENTRY_DSN` | Sentry | Dashboard Sentry → Projeto → DSN |
|
|
62
72
|
| `MIXPANEL_TOKEN` | Mixpanel | Dashboard Mixpanel → Configurações → Token |
|
|
63
73
|
|
|
@@ -175,6 +185,6 @@ Sem Mac: [docs/codemagic-release.md](docs/codemagic-release.md)
|
|
|
175
185
|
|
|
176
186
|
## Segurança
|
|
177
187
|
|
|
178
|
-
O `.gitignore` já exclui: `firebase_key.json`, `.env`, `.env.*`, `*.pem`, `*.keystore`, `.kasy/apple.env`, `.kasy/codemagic.env`.
|
|
188
|
+
O `.gitignore` já exclui: `firebase_key.json`, `.env`, `.env.*`, `*.pem`, `*.keystore`, `.kasy/apple.env`, `.kasy/codemagic.env`, `.kasy/*.log`.
|
|
179
189
|
|
|
180
190
|
Nunca comite credenciais no repositório.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
package com.aicrus.firebase.kit
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
-
import android.content.Intent
|
|
5
4
|
import androidx.compose.runtime.Composable
|
|
6
5
|
import androidx.compose.ui.graphics.Color
|
|
7
6
|
import androidx.compose.ui.unit.dp
|
|
8
7
|
import androidx.compose.ui.unit.sp
|
|
8
|
+
import androidx.core.content.ContextCompat
|
|
9
9
|
import androidx.glance.GlanceId
|
|
10
10
|
import androidx.glance.GlanceModifier
|
|
11
11
|
import androidx.glance.Image
|
|
@@ -14,7 +14,7 @@ import androidx.glance.LocalSize
|
|
|
14
14
|
import androidx.glance.action.clickable
|
|
15
15
|
import androidx.glance.appwidget.GlanceAppWidget
|
|
16
16
|
import androidx.glance.appwidget.SizeMode
|
|
17
|
-
import androidx.glance.appwidget.action.
|
|
17
|
+
import androidx.glance.appwidget.action.actionRunCallback
|
|
18
18
|
import androidx.glance.appwidget.provideContent
|
|
19
19
|
import androidx.glance.background
|
|
20
20
|
import androidx.glance.currentState
|
|
@@ -36,7 +36,6 @@ import androidx.glance.text.TextStyle
|
|
|
36
36
|
import androidx.glance.unit.ColorProvider
|
|
37
37
|
import es.antonborri.home_widget.HomeWidgetGlanceState
|
|
38
38
|
import es.antonborri.home_widget.HomeWidgetGlanceStateDefinition
|
|
39
|
-
import java.util.Calendar
|
|
40
39
|
import java.util.Locale
|
|
41
40
|
|
|
42
41
|
class MyWidgetWidget : GlanceAppWidget() {
|
|
@@ -62,24 +61,50 @@ class MyWidgetWidget : GlanceAppWidget() {
|
|
|
62
61
|
val planText = prefs.getString("planText", "") ?: ""
|
|
63
62
|
val isPro = prefs.getString("isPro", "false") == "true"
|
|
64
63
|
val quote = prefs.getString("quote", "") ?: ""
|
|
64
|
+
val quoteAuthor = prefs.getString("quoteAuthor", "") ?: ""
|
|
65
65
|
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
66
|
+
// Fallback used when Flutter has not pushed data yet — first install
|
|
67
|
+
// before the app opens. Keeps the widget from rendering blank in the
|
|
68
|
+
// gallery preview.
|
|
69
69
|
val defaults = defaultStrings()
|
|
70
70
|
val greeting = storedGreeting.ifEmpty { defaults.greeting }
|
|
71
71
|
val title = storedTitle.ifEmpty { defaults.hello }
|
|
72
72
|
|
|
73
73
|
val size = LocalSize.current
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
val
|
|
74
|
+
// "Narrow" covers both the true small widget and the tall-but-narrow
|
|
75
|
+
// single-column shape — both get the tightest sentence count.
|
|
76
|
+
val isSmall = size.width < 240.dp
|
|
77
|
+
// We show N COMPLETE sentences from the quote (split on "\n" — which is
|
|
78
|
+
// how the i18n JSON separates the four sentences). Truncating per
|
|
79
|
+
// sentence avoids the ugly "…" mid-word that lineLimit alone produces.
|
|
80
|
+
// height < 180dp → no quote.
|
|
81
|
+
// narrow column → 1 sentence (just the first one).
|
|
82
|
+
// wide + short → 2 sentences.
|
|
83
|
+
// wide + medium height → 3 sentences.
|
|
84
|
+
// wide + tall → 4 sentences + bold author.
|
|
85
|
+
val showQuote = size.height >= 180.dp
|
|
86
|
+
val numSentences = when {
|
|
87
|
+
isSmall -> 1
|
|
88
|
+
size.height < 240.dp -> 2
|
|
89
|
+
size.height < 300.dp -> 3
|
|
90
|
+
else -> 4
|
|
91
|
+
}
|
|
92
|
+
val showQuoteAuthor = !isSmall && size.height >= 300.dp
|
|
93
|
+
val sentences = quote.split("\n").filter { it.isNotBlank() }
|
|
94
|
+
val displayQuote = sentences.take(numSentences).joinToString("\n")
|
|
95
|
+
// Each sentence is short enough to wrap to 2 lines on narrow widgets,
|
|
96
|
+
// so a 2x cap covers the worst case without slicing the last sentence.
|
|
97
|
+
val quoteMaxLines = numSentences * 2
|
|
77
98
|
|
|
99
|
+
// Brand colors. The pure-Compose colors (whiteSubtle, whiteQuote,
|
|
100
|
+
// whiteVerySubtle) are foreground tints used by the live widget only,
|
|
101
|
+
// so they stay inline. The gold pulls from res/values/colors.xml to keep
|
|
102
|
+
// a single source of truth shared with the XML drawables.
|
|
78
103
|
val white = Color.White
|
|
79
104
|
val whiteSubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.55f)
|
|
80
105
|
val whiteQuote = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.65f)
|
|
81
|
-
val gold = Color(red = 1f, green = 0.84f, blue = 0f)
|
|
82
106
|
val whiteVerySubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.45f)
|
|
107
|
+
val gold = Color(ContextCompat.getColor(context, R.color.widget_pro_gold))
|
|
83
108
|
|
|
84
109
|
// The gradient lives in its own Image at the bottom of the stack rather
|
|
85
110
|
// than as `.background(ImageProvider(...))`, because in some Glance
|
|
@@ -89,7 +114,7 @@ class MyWidgetWidget : GlanceAppWidget() {
|
|
|
89
114
|
Box(
|
|
90
115
|
modifier = GlanceModifier
|
|
91
116
|
.fillMaxSize()
|
|
92
|
-
.clickable(
|
|
117
|
+
.clickable(actionRunCallback<OpenAppAction>()),
|
|
93
118
|
) {
|
|
94
119
|
Image(
|
|
95
120
|
provider = ImageProvider(R.drawable.widget_gradient_inner),
|
|
@@ -117,18 +142,37 @@ class MyWidgetWidget : GlanceAppWidget() {
|
|
|
117
142
|
),
|
|
118
143
|
modifier = GlanceModifier.padding(top = 4.dp),
|
|
119
144
|
)
|
|
120
|
-
if (
|
|
145
|
+
if (showQuote && displayQuote.isNotEmpty()) {
|
|
121
146
|
Text(
|
|
122
|
-
text =
|
|
147
|
+
text = displayQuote,
|
|
123
148
|
style = TextStyle(
|
|
124
149
|
color = ColorProvider(whiteQuote),
|
|
125
150
|
fontSize = 15.sp,
|
|
126
151
|
fontWeight = FontWeight.Normal,
|
|
127
152
|
fontStyle = FontStyle.Italic,
|
|
128
153
|
),
|
|
129
|
-
maxLines =
|
|
154
|
+
maxLines = quoteMaxLines,
|
|
130
155
|
modifier = GlanceModifier.padding(top = 12.dp),
|
|
131
156
|
)
|
|
157
|
+
if (showQuoteAuthor && quoteAuthor.isNotEmpty()) {
|
|
158
|
+
// Author goes on its own line, right-aligned and bold, so the
|
|
159
|
+
// quote reads as a quote and the attribution is visually distinct.
|
|
160
|
+
Row(
|
|
161
|
+
modifier = GlanceModifier
|
|
162
|
+
.fillMaxWidth()
|
|
163
|
+
.padding(top = 4.dp),
|
|
164
|
+
horizontalAlignment = Alignment.End,
|
|
165
|
+
) {
|
|
166
|
+
Text(
|
|
167
|
+
text = quoteAuthor,
|
|
168
|
+
style = TextStyle(
|
|
169
|
+
color = ColorProvider(whiteQuote),
|
|
170
|
+
fontSize = 13.sp,
|
|
171
|
+
fontWeight = FontWeight.Bold,
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
132
176
|
}
|
|
133
177
|
Spacer(modifier = GlanceModifier.defaultWeight())
|
|
134
178
|
Row(
|
|
@@ -153,7 +197,7 @@ class MyWidgetWidget : GlanceAppWidget() {
|
|
|
153
197
|
contentDescription = "Add",
|
|
154
198
|
modifier = GlanceModifier
|
|
155
199
|
.size(34.dp)
|
|
156
|
-
.clickable(
|
|
200
|
+
.clickable(actionRunCallback<OpenAppAction>()),
|
|
157
201
|
)
|
|
158
202
|
}
|
|
159
203
|
}
|
|
@@ -201,37 +245,16 @@ class MyWidgetWidget : GlanceAppWidget() {
|
|
|
201
245
|
}
|
|
202
246
|
}
|
|
203
247
|
|
|
248
|
+
// Fallback used ONLY in the brief window between the widget being placed and
|
|
249
|
+
// the Flutter app pushing real values. Keep this dead simple — the
|
|
250
|
+
// time-aware greeting in three languages lives on the Dart side
|
|
251
|
+
// (home_widget_mywidget_service.dart::_greeting). Duplicating that logic
|
|
252
|
+
// here was a maintenance trap when adding new locales.
|
|
204
253
|
private data class DefaultStrings(val greeting: String, val hello: String)
|
|
205
254
|
|
|
206
|
-
private fun defaultStrings(): DefaultStrings {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
when (lang) {
|
|
211
|
-
"pt" -> { morning = "Bom dia"; afternoon = "Boa tarde"; evening = "Boa noite"; hello = "Olá!" }
|
|
212
|
-
"es" -> { morning = "Buenos días"; afternoon = "Buenas tardes"; evening = "Buenas noches"; hello = "¡Hola!" }
|
|
213
|
-
else -> { morning = "Good morning"; afternoon = "Good afternoon"; evening = "Good evening"; hello = "Hi there!" }
|
|
214
|
-
}
|
|
215
|
-
val greeting = when {
|
|
216
|
-
hour < 12 -> morning
|
|
217
|
-
hour < 18 -> afternoon
|
|
218
|
-
else -> evening
|
|
219
|
-
}
|
|
220
|
-
return DefaultStrings(greeting, hello)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/// Builds the exact Intent the system launcher fires when the user taps
|
|
224
|
-
/// the app icon. We must NOT add extra flags here — getLaunchIntentForPackage
|
|
225
|
-
/// already returns `FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_IF_NEEDED`,
|
|
226
|
-
/// which is the same combo the launcher uses. Adding `CLEAR_TOP` destroys
|
|
227
|
-
/// go_router's navigation stack on warm starts and lands the user on the
|
|
228
|
-
/// errorBuilder ("404 - Page not found").
|
|
229
|
-
private fun launchAppIntent(context: Context): Intent {
|
|
230
|
-
return context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
231
|
-
?: Intent(context, MainActivity::class.java).apply {
|
|
232
|
-
action = Intent.ACTION_MAIN
|
|
233
|
-
addCategory(Intent.CATEGORY_LAUNCHER)
|
|
234
|
-
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
235
|
-
}
|
|
255
|
+
private fun defaultStrings(): DefaultStrings = when (Locale.getDefault().language) {
|
|
256
|
+
"pt" -> DefaultStrings(greeting = "Olá", hello = "Bem-vindo!")
|
|
257
|
+
"es" -> DefaultStrings(greeting = "Hola", hello = "¡Bienvenido!")
|
|
258
|
+
else -> DefaultStrings(greeting = "Hello", hello = "Welcome!")
|
|
236
259
|
}
|
|
237
260
|
}
|
package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidgetReceiver.kt
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
package com.aicrus.firebase.kit
|
|
2
2
|
|
|
3
|
+
import android.appwidget.AppWidgetManager
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import androidx.glance.appwidget.GlanceAppWidgetManager
|
|
3
6
|
import es.antonborri.home_widget.HomeWidgetGlanceWidgetReceiver
|
|
7
|
+
import kotlinx.coroutines.runBlocking
|
|
4
8
|
|
|
5
9
|
class MyWidgetReceiver : HomeWidgetGlanceWidgetReceiver<MyWidgetWidget>() {
|
|
6
10
|
override val glanceAppWidget = MyWidgetWidget()
|
|
11
|
+
|
|
12
|
+
// Override the parent's onUpdate because its updateAppWidgetState
|
|
13
|
+
// transformation returns the SAME state instance back. Glance compares
|
|
14
|
+
// references, treats the state as unchanged, and skips the recomposition.
|
|
15
|
+
// The symptom on the Flutter side: first language switch does not update,
|
|
16
|
+
// user has to tap twice. We force a fresh recompose by calling update()
|
|
17
|
+
// directly on each widget id — the composable then reads the latest
|
|
18
|
+
// SharedPreferences values that Dart already committed synchronously.
|
|
19
|
+
override fun onUpdate(
|
|
20
|
+
context: Context,
|
|
21
|
+
appWidgetManager: AppWidgetManager,
|
|
22
|
+
appWidgetIds: IntArray,
|
|
23
|
+
) {
|
|
24
|
+
runBlocking {
|
|
25
|
+
val manager = GlanceAppWidgetManager(context)
|
|
26
|
+
appWidgetIds.forEach { widgetId ->
|
|
27
|
+
val glanceId = manager.getGlanceIdBy(widgetId)
|
|
28
|
+
glanceAppWidget.update(context, glanceId)
|
|
29
|
+
}
|
|
30
|
+
// Defensive 2nd update: Glance occasionally coalesces or skips the
|
|
31
|
+
// first invalidation when the user triggers two updates in quick
|
|
32
|
+
// succession (e.g. tapping the language tile and immediately
|
|
33
|
+
// returning to the home screen). A short gap + second update
|
|
34
|
+
// guarantees the freshly committed SharedPreferences values
|
|
35
|
+
// actually reach the composition. The symptom without this:
|
|
36
|
+
// first language tap appears to do nothing, second tap "fixes" it.
|
|
37
|
+
kotlinx.coroutines.delay(200)
|
|
38
|
+
appWidgetIds.forEach { widgetId ->
|
|
39
|
+
val glanceId = manager.getGlanceIdBy(widgetId)
|
|
40
|
+
glanceAppWidget.update(context, glanceId)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
7
44
|
}
|
package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/OpenAppAction.kt
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package com.aicrus.firebase.kit
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import androidx.glance.GlanceId
|
|
6
|
+
import androidx.glance.action.ActionParameters
|
|
7
|
+
import androidx.glance.appwidget.action.ActionCallback
|
|
8
|
+
|
|
9
|
+
// Custom ActionCallback used by MyWidgetWidget instead of actionStartActivity.
|
|
10
|
+
// actionStartActivity wraps the launch in an internal "glance-action://CALLBACK"
|
|
11
|
+
// intent that ends up reaching MainActivity (because launchMode=singleTop) as
|
|
12
|
+
// the new intent — Flutter sees that URI as a deep link and go_router blows up
|
|
13
|
+
// with "no routes for location: glance-action:/CALLBACK". Running the launch
|
|
14
|
+
// ourselves through onAction bypasses that wrapping entirely.
|
|
15
|
+
class OpenAppAction : ActionCallback {
|
|
16
|
+
override suspend fun onAction(
|
|
17
|
+
context: Context,
|
|
18
|
+
glanceId: GlanceId,
|
|
19
|
+
parameters: ActionParameters,
|
|
20
|
+
) {
|
|
21
|
+
val intent = Intent(context, MainActivity::class.java).apply {
|
|
22
|
+
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
|
23
|
+
}
|
|
24
|
+
context.startActivity(intent)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
android:viewportHeight="34">
|
|
7
7
|
<path
|
|
8
8
|
android:pathData="M17,0 A17,17 0 1,0 17,34 A17,17 0 1,0 17,0 Z"
|
|
9
|
-
android:fillColor="
|
|
9
|
+
android:fillColor="@color/widget_add_button_bg"/>
|
|
10
10
|
<path
|
|
11
11
|
android:pathData="M17,9 L17,25 M9,17 L25,17"
|
|
12
|
-
android:strokeColor="
|
|
12
|
+
android:strokeColor="@color/widget_add_button_stroke"
|
|
13
13
|
android:strokeWidth="2.5"
|
|
14
14
|
android:strokeLineCap="round"/>
|
|
15
15
|
</vector>
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Background used by the static preview / loading layouts (RemoteViews
|
|
3
|
+
XML rendered by the launcher BEFORE Glance composes the real widget).
|
|
4
|
+
Includes the corner radius because the launcher doesn't clip these
|
|
5
|
+
layouts the same way it clips the live Glance widget.
|
|
6
|
+
To restyle the brand gradient, edit res/values/colors.xml. -->
|
|
2
7
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
|
3
8
|
<gradient
|
|
4
9
|
android:angle="315"
|
|
5
|
-
android:startColor="
|
|
6
|
-
android:endColor="
|
|
10
|
+
android:startColor="@color/widget_gradient_start"
|
|
11
|
+
android:endColor="@color/widget_gradient_end"
|
|
7
12
|
android:type="linear" />
|
|
8
13
|
<corners android:radius="24dp" />
|
|
9
14
|
</shape>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<!--
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
<!-- Background of the live Glance widget. No corners here — the OS already
|
|
3
|
+
clips the widget with its corner radius, so adding rounded corners
|
|
4
|
+
would produce a visible double-radius edge. To restyle the brand
|
|
5
|
+
gradient, edit res/values/colors.xml. -->
|
|
6
6
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
|
7
7
|
<gradient
|
|
8
8
|
android:angle="315"
|
|
9
|
-
android:startColor="
|
|
10
|
-
android:endColor="
|
|
9
|
+
android:startColor="@color/widget_gradient_start"
|
|
10
|
+
android:endColor="@color/widget_gradient_end"
|
|
11
11
|
android:type="linear" />
|
|
12
12
|
</shape>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
2
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
|
3
3
|
<corners android:radius="999dp"/>
|
|
4
|
-
<solid android:color="
|
|
4
|
+
<solid android:color="@color/widget_free_pill_bg"/>
|
|
5
5
|
</shape>
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<shape android:shape="rectangle">
|
|
9
9
|
<gradient
|
|
10
10
|
android:angle="315"
|
|
11
|
-
android:startColor="
|
|
12
|
-
android:endColor="
|
|
11
|
+
android:startColor="@color/widget_gradient_start"
|
|
12
|
+
android:endColor="@color/widget_gradient_end"
|
|
13
13
|
android:type="linear" />
|
|
14
14
|
<corners android:radius="24dp" />
|
|
15
15
|
</shape>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
2
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
|
3
3
|
<corners android:radius="999dp"/>
|
|
4
|
-
<solid android:color="
|
|
4
|
+
<solid android:color="@color/widget_pro_pill_bg"/>
|
|
5
5
|
</shape>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png
CHANGED
|
Binary file
|
|
Binary file
|
package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png
CHANGED
|
Binary file
|
|
Binary file
|