kasy-cli 1.15.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/icon.js +29 -1
- package/lib/commands/new.js +34 -8
- package/lib/commands/remove.js +14 -3
- package/lib/commands/run.js +264 -3
- package/lib/scaffold/CHANGELOG.json +9 -0
- package/lib/scaffold/backends/api/patch/README.md +3 -2
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +2 -0
- package/lib/scaffold/backends/supabase/patch/README.md +3 -2
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +2 -0
- 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 +34 -2
- package/lib/utils/i18n/messages-es.js +34 -2
- package/lib/utils/i18n/messages-pt.js +34 -2
- package/lib/utils/png-padding.js +134 -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/android/app/src/main/res/drawable-hdpi/android12splash.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-night-hdpi/android12splash.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-xhdpi/android12splash.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-xxxhdpi/android12splash.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-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/assets/images/splash_logo_dark_android12.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/lib/core/home_widgets/home_widget_mywidget_service.dart +39 -41
- package/templates/firebase/lib/router.dart +15 -1
- package/templates/firebase/web/index.html +3 -0
package/lib/utils/png-padding.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const fsp = require('node:fs/promises');
|
|
2
2
|
const { PNG } = require('pngjs');
|
|
3
3
|
|
|
4
|
-
const ANDROID12_SAFE_RATIO = 0.
|
|
4
|
+
const ANDROID12_SAFE_RATIO = 0.4;
|
|
5
|
+
const ANDROID_ADAPTIVE_LOGO_RATIO = 0.65;
|
|
6
|
+
const ANDROID_ADAPTIVE_CANVAS = 1024;
|
|
5
7
|
|
|
6
8
|
async function readPng(filePath) {
|
|
7
9
|
const buffer = await fsp.readFile(filePath);
|
|
@@ -117,4 +119,134 @@ async function writeAndroid12Variant(srcPath, dstPath, safeRatio = ANDROID12_SAF
|
|
|
117
119
|
return { canvasSize, logoW, logoH };
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
function sampleSolidBackground(png) {
|
|
123
|
+
const inset = Math.max(2, Math.floor(Math.min(png.width, png.height) * 0.02));
|
|
124
|
+
const corners = [
|
|
125
|
+
[inset, inset],
|
|
126
|
+
[png.width - inset - 1, inset],
|
|
127
|
+
[inset, png.height - inset - 1],
|
|
128
|
+
[png.width - inset - 1, png.height - inset - 1],
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
let r = 0;
|
|
132
|
+
let g = 0;
|
|
133
|
+
let b = 0;
|
|
134
|
+
let opaque = 0;
|
|
135
|
+
for (const [x, y] of corners) {
|
|
136
|
+
const idx = (y * png.width + x) * 4;
|
|
137
|
+
const alpha = png.data[idx + 3];
|
|
138
|
+
if (alpha < 250) continue;
|
|
139
|
+
r += png.data[idx];
|
|
140
|
+
g += png.data[idx + 1];
|
|
141
|
+
b += png.data[idx + 2];
|
|
142
|
+
opaque += 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (opaque === 0) {
|
|
146
|
+
return { r: 255, g: 255, b: 255, sampled: false };
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
r: Math.round(r / opaque),
|
|
150
|
+
g: Math.round(g / opaque),
|
|
151
|
+
b: Math.round(b / opaque),
|
|
152
|
+
sampled: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function fillSolidColor(png, color) {
|
|
157
|
+
for (let i = 0; i < png.data.length; i += 4) {
|
|
158
|
+
png.data[i] = color.r;
|
|
159
|
+
png.data[i + 1] = color.g;
|
|
160
|
+
png.data[i + 2] = color.b;
|
|
161
|
+
png.data[i + 3] = 255;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function compositeLogoOnBackground(background, logo) {
|
|
166
|
+
const offsetX = Math.floor((background.width - logo.width) / 2);
|
|
167
|
+
const offsetY = Math.floor((background.height - logo.height) / 2);
|
|
168
|
+
|
|
169
|
+
for (let y = 0; y < logo.height; y++) {
|
|
170
|
+
for (let x = 0; x < logo.width; x++) {
|
|
171
|
+
const srcIdx = (y * logo.width + x) * 4;
|
|
172
|
+
const alpha = logo.data[srcIdx + 3] / 255;
|
|
173
|
+
if (alpha === 0) continue;
|
|
174
|
+
|
|
175
|
+
const dstIdx = ((y + offsetY) * background.width + (x + offsetX)) * 4;
|
|
176
|
+
const inv = 1 - alpha;
|
|
177
|
+
background.data[dstIdx] = Math.round(logo.data[srcIdx] * alpha + background.data[dstIdx] * inv);
|
|
178
|
+
background.data[dstIdx + 1] = Math.round(logo.data[srcIdx + 1] * alpha + background.data[dstIdx + 1] * inv);
|
|
179
|
+
background.data[dstIdx + 2] = Math.round(logo.data[srcIdx + 2] * alpha + background.data[dstIdx + 2] * inv);
|
|
180
|
+
background.data[dstIdx + 3] = 255;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Generate the Android adaptive icon background asset. Reads the user-supplied
|
|
187
|
+
* icon PNG, samples a solid background color from its corners (falling back to
|
|
188
|
+
* white when the image is transparent), and writes a square canvas with the
|
|
189
|
+
* logo scaled down and centered. The result is what `flutter_launcher_icons`
|
|
190
|
+
* passes to `adaptive_icon_background`, fully filling the launcher's circular
|
|
191
|
+
* mask without the default white halo around square icons.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} srcPath source icon PNG path
|
|
194
|
+
* @param {string} dstPath output path for icon_android.png
|
|
195
|
+
* @param {object} [options]
|
|
196
|
+
* @param {number} [options.canvasSize] output square size (default 1024)
|
|
197
|
+
* @param {number} [options.logoRatio] logo fill ratio inside canvas (default 0.65)
|
|
198
|
+
* @param {string} [options.backgroundColor] explicit hex like "#01171f" to override sampling
|
|
199
|
+
*/
|
|
200
|
+
async function writeAndroidAdaptiveBackground(srcPath, dstPath, options = {}) {
|
|
201
|
+
const canvasSize = options.canvasSize || ANDROID_ADAPTIVE_CANVAS;
|
|
202
|
+
const logoRatio = options.logoRatio || ANDROID_ADAPTIVE_LOGO_RATIO;
|
|
203
|
+
|
|
204
|
+
const src = await readPng(srcPath);
|
|
205
|
+
|
|
206
|
+
let color;
|
|
207
|
+
if (options.backgroundColor) {
|
|
208
|
+
const hex = options.backgroundColor.replace('#', '');
|
|
209
|
+
color = {
|
|
210
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
211
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
212
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
213
|
+
sampled: false,
|
|
214
|
+
};
|
|
215
|
+
} else {
|
|
216
|
+
color = sampleSolidBackground(src);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const canvas = new PNG({ width: canvasSize, height: canvasSize });
|
|
220
|
+
fillSolidColor(canvas, color);
|
|
221
|
+
|
|
222
|
+
const logoSize = Math.round(canvasSize * logoRatio);
|
|
223
|
+
const resized = resizeBilinear(src, logoSize, logoSize);
|
|
224
|
+
compositeLogoOnBackground(canvas, resized);
|
|
225
|
+
|
|
226
|
+
await writePng(canvas, dstPath);
|
|
227
|
+
return { canvasSize, logoSize, color };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Write a fully transparent square PNG. Used to satisfy
|
|
232
|
+
* `adaptive_icon_foreground` when the entire image already lives in the
|
|
233
|
+
* background layer.
|
|
234
|
+
*
|
|
235
|
+
* @param {string} dstPath
|
|
236
|
+
* @param {number} [size]
|
|
237
|
+
*/
|
|
238
|
+
async function writeTransparentSquare(dstPath, size = ANDROID_ADAPTIVE_CANVAS) {
|
|
239
|
+
const png = new PNG({ width: size, height: size });
|
|
240
|
+
png.data.fill(0);
|
|
241
|
+
await writePng(png, dstPath);
|
|
242
|
+
return { size };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = {
|
|
246
|
+
writeAndroid12Variant,
|
|
247
|
+
writeAndroidAdaptiveBackground,
|
|
248
|
+
writeTransparentSquare,
|
|
249
|
+
ANDROID12_SAFE_RATIO,
|
|
250
|
+
ANDROID_ADAPTIVE_LOGO_RATIO,
|
|
251
|
+
ANDROID_ADAPTIVE_CANVAS,
|
|
252
|
+
};
|
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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
package com.aicrus.firebase.kit
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
4
5
|
import android.os.Bundle
|
|
6
|
+
import android.util.Log
|
|
5
7
|
import androidx.appcompat.app.AppCompatDelegate
|
|
6
8
|
import com.google.android.gms.ads.identifier.AdvertisingIdClient
|
|
7
9
|
import io.flutter.embedding.android.FlutterActivity
|
|
@@ -20,6 +22,19 @@ class MainActivity : FlutterActivity() {
|
|
|
20
22
|
super.onCreate(savedInstanceState)
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
// Logs the incoming intent so we can diagnose warm-starts from the home
|
|
26
|
+
// widget that land on go_router's 404. With this, the Dart side's onException
|
|
27
|
+
// logs and these adb logs together show whether the intent itself carries a
|
|
28
|
+
// bad URI (data Uri / extras) or whether the 404 comes from elsewhere.
|
|
29
|
+
override fun onNewIntent(intent: Intent) {
|
|
30
|
+
Log.d(
|
|
31
|
+
"KasyWidgetTap",
|
|
32
|
+
"onNewIntent action=${intent.action} data=${intent.data} " +
|
|
33
|
+
"flags=0x${Integer.toHexString(intent.flags)} extras=${intent.extras}",
|
|
34
|
+
)
|
|
35
|
+
super.onNewIntent(intent)
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
// Forces the night mode to match the user's saved theme preference (read
|
|
24
39
|
// from `shared_preferences`) so the native splash drawable selection
|
|
25
40
|
// (drawable-night vs drawable) follows the in-app choice, not just the OS.
|
|
@@ -1,7 +1,6 @@
|
|
|
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
|
|
@@ -14,7 +13,7 @@ import androidx.glance.LocalSize
|
|
|
14
13
|
import androidx.glance.action.clickable
|
|
15
14
|
import androidx.glance.appwidget.GlanceAppWidget
|
|
16
15
|
import androidx.glance.appwidget.SizeMode
|
|
17
|
-
import androidx.glance.appwidget.action.
|
|
16
|
+
import androidx.glance.appwidget.action.actionRunCallback
|
|
18
17
|
import androidx.glance.appwidget.provideContent
|
|
19
18
|
import androidx.glance.background
|
|
20
19
|
import androidx.glance.currentState
|
|
@@ -89,7 +88,7 @@ class MyWidgetWidget : GlanceAppWidget() {
|
|
|
89
88
|
Box(
|
|
90
89
|
modifier = GlanceModifier
|
|
91
90
|
.fillMaxSize()
|
|
92
|
-
.clickable(
|
|
91
|
+
.clickable(actionRunCallback<OpenAppAction>()),
|
|
93
92
|
) {
|
|
94
93
|
Image(
|
|
95
94
|
provider = ImageProvider(R.drawable.widget_gradient_inner),
|
|
@@ -153,7 +152,7 @@ class MyWidgetWidget : GlanceAppWidget() {
|
|
|
153
152
|
contentDescription = "Add",
|
|
154
153
|
modifier = GlanceModifier
|
|
155
154
|
.size(34.dp)
|
|
156
|
-
.clickable(
|
|
155
|
+
.clickable(actionRunCallback<OpenAppAction>()),
|
|
157
156
|
)
|
|
158
157
|
}
|
|
159
158
|
}
|
|
@@ -219,19 +218,4 @@ class MyWidgetWidget : GlanceAppWidget() {
|
|
|
219
218
|
}
|
|
220
219
|
return DefaultStrings(greeting, hello)
|
|
221
220
|
}
|
|
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
|
-
}
|
|
236
|
-
}
|
|
237
221
|
}
|
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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png
CHANGED
|
Binary file
|
package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png
CHANGED
|
Binary file
|
package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -9,12 +9,24 @@ Guía para activar suscripciones y compras en la app después de que la CLI gene
|
|
|
9
9
|
| Ya listo | Lo que falta |
|
|
10
10
|
|----------|--------------|
|
|
11
11
|
| Instaló `purchases_flutter` | Crear cuenta en RevenueCat |
|
|
12
|
-
| Configuró las claves en `
|
|
12
|
+
| Configuró las claves en `.env` (test/iOS prod/Android prod) | Crear Productos, Entitlements y Offerings en el panel RC |
|
|
13
13
|
| Generó el código del paywall y del repositorio de suscripciones | Registrar la URL del webhook en el panel RC |
|
|
14
14
|
| Firebase: desplegó la Cloud Function del webhook | — |
|
|
15
15
|
| Supabase: desplegó la Edge Function del webhook | — |
|
|
16
16
|
|
|
17
|
-
> Las claves quedan en `.vscode/launch.json`
|
|
17
|
+
> Las claves quedan en `.env` en la raíz (fuente de verdad) y reflejadas en `.vscode/launch.json` + `Makefile`. Todos en `.gitignore` — nunca van al repositorio.
|
|
18
|
+
|
|
19
|
+
### ¿Qué clave usar?
|
|
20
|
+
|
|
21
|
+
La CLI pregunta **tres claves opcionales** (al menos una es obligatoria):
|
|
22
|
+
|
|
23
|
+
| Variable | Prefijo | Uso |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `RC_TEST_KEY` | `test_` | Test Store. **Una sola clave**, sirve para iOS+Android. Usada automáticamente en simulador/emulador. |
|
|
26
|
+
| `RC_IOS_PROD_KEY` | `appl_` | App Store (Sandbox + Producción). Usada automáticamente en iPhone físico. |
|
|
27
|
+
| `RC_ANDROID_PROD_KEY` | `goog_` | Google Play (Sandbox + Producción). Usada automáticamente en Android físico. |
|
|
28
|
+
|
|
29
|
+
`kasy run` elige la clave correcta según el dispositivo. Forzar manualmente: `kasy run --rc=test` o `kasy run --rc=prod`.
|
|
18
30
|
|
|
19
31
|
---
|
|
20
32
|
|
|
@@ -155,9 +167,13 @@ Después de completar todo y configurar el idioma del grupo, el estado sale de *
|
|
|
155
167
|
|
|
156
168
|
[app.revenuecat.com](https://app.revenuecat.com) → tu proyecto → **Apps** → `+ Add app` → **App Store** → copia la clave `appl_xxx`.
|
|
157
169
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
170
|
+
Pégala en el `.env` de la raíz:
|
|
171
|
+
|
|
172
|
+
```env
|
|
173
|
+
RC_IOS_PROD_KEY=appl_xxxxxxxxxxxxxxx
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`kasy run` usa esta clave automáticamente en iPhone físico (el simulador sigue con `RC_TEST_KEY`).
|
|
161
177
|
|
|
162
178
|
**6. Crear Sandbox Tester — obligatorio para probar en iPhone físico**
|
|
163
179
|
|
|
@@ -253,9 +269,13 @@ d) Google Play Console → **Configuración** → **Usuarios y permisos** → **
|
|
|
253
269
|
|
|
254
270
|
[app.revenuecat.com](https://app.revenuecat.com) → tu proyecto → **Apps** → `+ Add app` → **Google Play** → sube el archivo JSON → copia la clave `goog_xxx`.
|
|
255
271
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
272
|
+
Pégala en el `.env` de la raíz:
|
|
273
|
+
|
|
274
|
+
```env
|
|
275
|
+
RC_ANDROID_PROD_KEY=goog_xxxxxxxxxxxxxxx
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`kasy run` usa esta clave automáticamente en Android físico (el emulador sigue con `RC_TEST_KEY`).
|
|
259
279
|
|
|
260
280
|
**8. Agregar License Tester — obligatorio para probar en el dispositivo**
|
|
261
281
|
|
|
@@ -9,12 +9,24 @@ Guia para ativar assinaturas e compras no app depois que a CLI gerou o projeto.
|
|
|
9
9
|
| Já pronto | O que ainda falta |
|
|
10
10
|
|-----------|-------------------|
|
|
11
11
|
| Instalou `purchases_flutter` | Criar conta no RevenueCat |
|
|
12
|
-
| Configurou as chaves no `
|
|
12
|
+
| Configurou as chaves no `.env` (test/iOS prod/Android prod) | Criar Produtos, Entitlements e Offerings no painel RC |
|
|
13
13
|
| Gerou o código do paywall e do repositório de assinaturas | Registrar a URL do webhook no painel RC |
|
|
14
14
|
| Firebase: implantou a Cloud Function do webhook | — |
|
|
15
15
|
| Supabase: implantou a Edge Function do webhook | — |
|
|
16
16
|
|
|
17
|
-
> As chaves ficam em `.vscode/launch.json`
|
|
17
|
+
> As chaves ficam em `.env` na raiz (fonte da verdade) e refletidas no `.vscode/launch.json` + `Makefile`. Todos no `.gitignore` — nunca vão para o repositório.
|
|
18
|
+
|
|
19
|
+
### Qual chave usar?
|
|
20
|
+
|
|
21
|
+
A CLI pergunta **três chaves opcionais** (pelo menos uma é obrigatória):
|
|
22
|
+
|
|
23
|
+
| Variável | Prefixo | Uso |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `RC_TEST_KEY` | `test_` | Test Store. **Uma chave única**, vale iOS+Android. Usada automaticamente em simulador/emulador. |
|
|
26
|
+
| `RC_IOS_PROD_KEY` | `appl_` | App Store (Sandbox + Produção). Usada automaticamente em iPhone físico. |
|
|
27
|
+
| `RC_ANDROID_PROD_KEY` | `goog_` | Google Play (Sandbox + Produção). Usada automaticamente em Android físico. |
|
|
28
|
+
|
|
29
|
+
O `kasy run` escolhe a chave certa baseado no device. Forçar manual: `kasy run --rc=test` ou `kasy run --rc=prod`.
|
|
18
30
|
|
|
19
31
|
---
|
|
20
32
|
|
|
@@ -155,9 +167,13 @@ Após preencher tudo e configurar o idioma do grupo, o status sai de **Missing M
|
|
|
155
167
|
|
|
156
168
|
[app.revenuecat.com](https://app.revenuecat.com) → seu projeto → **Apps** → `+ Add app` → **App Store** → copie a chave `appl_xxx`.
|
|
157
169
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
170
|
+
Cole no `.env` da raiz:
|
|
171
|
+
|
|
172
|
+
```env
|
|
173
|
+
RC_IOS_PROD_KEY=appl_xxxxxxxxxxxxxxx
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
O `kasy run` usa essa chave automaticamente em iPhone físico (simulador continua com `RC_TEST_KEY`).
|
|
161
177
|
|
|
162
178
|
**6. Criar Sandbox Tester — obrigatório para testar no iPhone físico**
|
|
163
179
|
|
|
@@ -253,9 +269,13 @@ d) Google Play Console → **Configurações** → **Usuários e permissões**
|
|
|
253
269
|
|
|
254
270
|
[app.revenuecat.com](https://app.revenuecat.com) → seu projeto → **Apps** → `+ Add app` → **Google Play** → faça upload do arquivo JSON → copie a chave `goog_xxx`.
|
|
255
271
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
272
|
+
Cole no `.env` da raiz:
|
|
273
|
+
|
|
274
|
+
```env
|
|
275
|
+
RC_ANDROID_PROD_KEY=goog_xxxxxxxxxxxxxxx
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
O `kasy run` usa essa chave automaticamente em Android físico (emulador continua com `RC_TEST_KEY`).
|
|
259
279
|
|
|
260
280
|
**8. Adicionar License Tester — obrigatório para testar no dispositivo**
|
|
261
281
|
|