kasy-cli 1.16.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kasy.js +1 -0
- package/lib/commands/add.js +45 -12
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/new.js +34 -8
- package/lib/commands/remove.js +14 -3
- package/lib/commands/run.js +207 -5
- package/lib/scaffold/CHANGELOG.json +9 -0
- package/lib/scaffold/backends/api/patch/README.md +3 -2
- package/lib/scaffold/backends/supabase/patch/README.md +3 -2
- package/lib/scaffold/shared/generator-utils.js +52 -8
- package/lib/scaffold/shared/post-build.js +105 -31
- package/lib/scaffold/shared/template-strings.js +6 -0
- package/lib/utils/i18n/messages-en.js +27 -2
- package/lib/utils/i18n/messages-es.js +27 -2
- package/lib/utils/i18n/messages-pt.js +27 -2
- package/package.json +1 -1
- package/templates/firebase/README.en.md +17 -7
- package/templates/firebase/README.es.md +17 -7
- package/templates/firebase/README.md +17 -7
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +15 -0
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +3 -19
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidgetReceiver.kt +37 -0
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/OpenAppAction.kt +26 -0
- package/templates/firebase/docs/revenuecat-setup.es.md +28 -8
- package/templates/firebase/docs/revenuecat-setup.pt.md +28 -8
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +39 -41
- package/templates/firebase/lib/router.dart +15 -1
|
@@ -665,49 +665,123 @@ async function validateFacebookAndroidStrings(projectDir) {
|
|
|
665
665
|
}
|
|
666
666
|
|
|
667
667
|
/**
|
|
668
|
-
*
|
|
669
|
-
*
|
|
668
|
+
* Read a project `.env` file into a plain object. Returns `{}` if the file is
|
|
669
|
+
* missing. Supports KEY=VALUE lines; ignores comments and blank lines.
|
|
670
670
|
*/
|
|
671
|
-
async function
|
|
672
|
-
const
|
|
673
|
-
if (!(await fs.pathExists(
|
|
674
|
-
return { ok: true, skipped: true, reason: 'no_makefile' };
|
|
675
|
-
}
|
|
676
|
-
|
|
671
|
+
async function readDotenv(projectDir) {
|
|
672
|
+
const envPath = path.join(projectDir, '.env');
|
|
673
|
+
if (!(await fs.pathExists(envPath))) return {};
|
|
677
674
|
let content;
|
|
678
675
|
try {
|
|
679
|
-
content = await fs.readFile(
|
|
680
|
-
} catch
|
|
681
|
-
return {
|
|
676
|
+
content = await fs.readFile(envPath, 'utf8');
|
|
677
|
+
} catch {
|
|
678
|
+
return {};
|
|
679
|
+
}
|
|
680
|
+
const env = {};
|
|
681
|
+
for (const rawLine of content.split('\n')) {
|
|
682
|
+
const line = rawLine.trim();
|
|
683
|
+
if (!line || line.startsWith('#')) continue;
|
|
684
|
+
const eq = line.indexOf('=');
|
|
685
|
+
if (eq === -1) continue;
|
|
686
|
+
const key = line.slice(0, eq).trim();
|
|
687
|
+
let value = line.slice(eq + 1).trim();
|
|
688
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
689
|
+
value = value.slice(1, -1);
|
|
690
|
+
}
|
|
691
|
+
if (key) env[key] = value;
|
|
682
692
|
}
|
|
693
|
+
return env;
|
|
694
|
+
}
|
|
683
695
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const iosTest = iosKey.startsWith('test_');
|
|
697
|
-
const androidTest = androidKey.startsWith('test_');
|
|
696
|
+
/**
|
|
697
|
+
* Validate RevenueCat keys. The new `.env` is the source of truth:
|
|
698
|
+
* RC_TEST_KEY (test_xxx — simulator/emulator)
|
|
699
|
+
* RC_IOS_PROD_KEY (appl_xxx — physical iOS)
|
|
700
|
+
* RC_ANDROID_PROD_KEY (goog_xxx — physical Android)
|
|
701
|
+
*
|
|
702
|
+
* Legacy fallback: if none of the new vars are set we read the old
|
|
703
|
+
* RC_ANDROID_API_KEY/RC_IOS_API_KEY from `.env`, and finally the Makefile
|
|
704
|
+
* (oldest projects). Returns flags `doctor.js` uses to render granular hints.
|
|
705
|
+
*/
|
|
706
|
+
async function validateRevenueCat(projectDir, config = {}) {
|
|
707
|
+
const env = await readDotenv(projectDir);
|
|
698
708
|
|
|
709
|
+
// Webhook URL (only Supabase exposes it directly; Firebase points to Console).
|
|
699
710
|
let webhookUrl = null;
|
|
700
711
|
if (config.backendProvider === 'supabase' && config.supabaseProjectId) {
|
|
701
712
|
webhookUrl = `https://${config.supabaseProjectId}.supabase.co/functions/v1/revenuecat-webhook`;
|
|
702
713
|
}
|
|
703
714
|
|
|
715
|
+
const testKey = (env.RC_TEST_KEY || '').trim();
|
|
716
|
+
const iosProdKey = (env.RC_IOS_PROD_KEY || '').trim();
|
|
717
|
+
const androidProdKey = (env.RC_ANDROID_PROD_KEY || '').trim();
|
|
718
|
+
const hasNewKeys = !!(testKey || iosProdKey || androidProdKey);
|
|
719
|
+
|
|
720
|
+
// Legacy single-key path (for projects generated before the test/prod split).
|
|
721
|
+
if (!hasNewKeys) {
|
|
722
|
+
let iosKey = (env.RC_IOS_API_KEY || '').trim();
|
|
723
|
+
let androidKey = (env.RC_ANDROID_API_KEY || '').trim();
|
|
724
|
+
|
|
725
|
+
// Last resort: read from Makefile (oldest projects).
|
|
726
|
+
if (!iosKey && !androidKey) {
|
|
727
|
+
const makefilePath = path.join(projectDir, 'Makefile');
|
|
728
|
+
if (await fs.pathExists(makefilePath)) {
|
|
729
|
+
try {
|
|
730
|
+
const content = await fs.readFile(makefilePath, 'utf8');
|
|
731
|
+
const activeLines = content.split('\n').filter((l) => !/^\s*#/.test(l)).join('\n');
|
|
732
|
+
const iosMatch = activeLines.match(/RC_IOS_API_KEY=([^\s\\]+)/);
|
|
733
|
+
const androidMatch = activeLines.match(/RC_ANDROID_API_KEY=([^\s\\]+)/);
|
|
734
|
+
iosKey = iosMatch ? iosMatch[1].trim() : '';
|
|
735
|
+
androidKey = androidMatch ? androidMatch[1].trim() : '';
|
|
736
|
+
} catch { /* ignore */ }
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const iosEmpty = !iosKey || iosKey === 'xxx';
|
|
741
|
+
const androidEmpty = !androidKey || androidKey === 'xxx';
|
|
742
|
+
const iosTest = iosKey.startsWith('test_');
|
|
743
|
+
const androidTest = androidKey.startsWith('test_');
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
ok: !iosEmpty && !androidEmpty,
|
|
747
|
+
legacy: true,
|
|
748
|
+
iosKey,
|
|
749
|
+
androidKey,
|
|
750
|
+
iosEmpty,
|
|
751
|
+
androidEmpty,
|
|
752
|
+
bothTest: iosTest && androidTest,
|
|
753
|
+
webhookUrl,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// New 3-key scheme.
|
|
758
|
+
const testOk = !!testKey && /^test_/.test(testKey);
|
|
759
|
+
const testBadPrefix = !!testKey && !/^test_/.test(testKey);
|
|
760
|
+
const iosProdOk = !!iosProdKey && /^appl_/.test(iosProdKey);
|
|
761
|
+
const iosProdBadPrefix = !!iosProdKey && !/^appl_/.test(iosProdKey);
|
|
762
|
+
const androidProdOk = !!androidProdKey && /^goog_/.test(androidProdKey);
|
|
763
|
+
const androidProdBadPrefix = !!androidProdKey && !/^goog_/.test(androidProdKey);
|
|
764
|
+
|
|
765
|
+
// OK if user can run at least one device flow end-to-end. Test alone is fine
|
|
766
|
+
// for development; prod-only configurations also work but skip the simulator.
|
|
767
|
+
const anyOk = testOk || iosProdOk || androidProdOk;
|
|
768
|
+
|
|
704
769
|
return {
|
|
705
|
-
ok:
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
770
|
+
ok: anyOk,
|
|
771
|
+
legacy: false,
|
|
772
|
+
testKey,
|
|
773
|
+
iosProdKey,
|
|
774
|
+
androidProdKey,
|
|
775
|
+
testOk,
|
|
776
|
+
testBadPrefix,
|
|
777
|
+
iosProdOk,
|
|
778
|
+
iosProdBadPrefix,
|
|
779
|
+
androidProdOk,
|
|
780
|
+
androidProdBadPrefix,
|
|
781
|
+
testMissing: !testKey,
|
|
782
|
+
iosProdMissing: !iosProdKey,
|
|
783
|
+
androidProdMissing: !androidProdKey,
|
|
784
|
+
onlyTest: testOk && !iosProdOk && !androidProdOk,
|
|
711
785
|
webhookUrl,
|
|
712
786
|
};
|
|
713
787
|
}
|
|
@@ -33,6 +33,8 @@ const TEMPLATE_STRINGS = {
|
|
|
33
33
|
supabase: '# Supabase',
|
|
34
34
|
apiRest: '# API REST',
|
|
35
35
|
revenuecat: '# RevenueCat',
|
|
36
|
+
revenuecatTest: '# Test Store key (test_xxx) — auto-used on simulator/emulator',
|
|
37
|
+
revenuecatProd: '# Production keys — auto-used on physical devices',
|
|
36
38
|
sentry: '# Sentry (prod only)',
|
|
37
39
|
mixpanel: '# Mixpanel',
|
|
38
40
|
llmChat: '# LLM Chat — Cloud/Edge Function endpoint (API key stays on the server, not here)',
|
|
@@ -65,6 +67,8 @@ const TEMPLATE_STRINGS = {
|
|
|
65
67
|
supabase: '# Supabase',
|
|
66
68
|
apiRest: '# API REST',
|
|
67
69
|
revenuecat: '# RevenueCat',
|
|
70
|
+
revenuecatTest: '# Chave Test Store (test_xxx) — usada automaticamente em simulador/emulador',
|
|
71
|
+
revenuecatProd: '# Chaves de produção — usadas automaticamente em dispositivo físico',
|
|
68
72
|
sentry: '# Sentry (apenas prod)',
|
|
69
73
|
mixpanel: '# Mixpanel',
|
|
70
74
|
llmChat: '# LLM Chat — endpoint da Cloud/Edge Function (a chave de API fica no servidor, não aqui)',
|
|
@@ -97,6 +101,8 @@ const TEMPLATE_STRINGS = {
|
|
|
97
101
|
supabase: '# Supabase',
|
|
98
102
|
apiRest: '# API REST',
|
|
99
103
|
revenuecat: '# RevenueCat',
|
|
104
|
+
revenuecatTest: '# Clave Test Store (test_xxx) — se usa automáticamente en simulador/emulador',
|
|
105
|
+
revenuecatProd: '# Claves de producción — se usan automáticamente en dispositivo físico',
|
|
100
106
|
sentry: '# Sentry (solo prod)',
|
|
101
107
|
mixpanel: '# Mixpanel',
|
|
102
108
|
llmChat: '# LLM Chat — endpoint de Cloud/Edge Function (la clave de API queda en el servidor, no aquí)',
|
|
@@ -419,8 +419,15 @@ module.exports = {
|
|
|
419
419
|
|
|
420
420
|
'doctor.revenuecat.title': 'RevenueCat',
|
|
421
421
|
'doctor.revenuecat.keysOk': 'API keys configured (iOS + Android)',
|
|
422
|
-
'doctor.revenuecat.keysEmpty': '
|
|
423
|
-
'doctor.revenuecat.
|
|
422
|
+
'doctor.revenuecat.keysEmpty': 'No keys configured — set at least RC_TEST_KEY in .env (kasy run uses it on simulator/emulator)',
|
|
423
|
+
'doctor.revenuecat.testKeyOk': 'RC_TEST_KEY configured (test_) — used on simulator/emulator',
|
|
424
|
+
'doctor.revenuecat.testKeyMissing': 'RC_TEST_KEY missing — subscription flow will not work on simulator/emulator',
|
|
425
|
+
'doctor.revenuecat.iosProdOk': 'RC_IOS_PROD_KEY configured (appl_) — used on physical iPhone',
|
|
426
|
+
'doctor.revenuecat.iosProdMissing': 'RC_IOS_PROD_KEY missing — kasy run on physical iPhone will fall back to the test key',
|
|
427
|
+
'doctor.revenuecat.androidProdOk': 'RC_ANDROID_PROD_KEY configured (goog_) — used on physical Android',
|
|
428
|
+
'doctor.revenuecat.androidProdMissing': 'RC_ANDROID_PROD_KEY missing — kasy run on physical Android will fall back to the test key',
|
|
429
|
+
'doctor.revenuecat.prefixMismatch': 'Key has wrong prefix: {key} should start with {expected}',
|
|
430
|
+
'doctor.revenuecat.keysTest': 'Only Test Store keys (test_) configured — store releases require appl_/goog_, otherwise the app crashes',
|
|
424
431
|
'doctor.revenuecat.webhookUrlSupabase': 'Webhook URL (paste in RevenueCat → Integrations → Webhooks)',
|
|
425
432
|
'doctor.revenuecat.webhookUrlFirebase': 'Webhook URL: Firebase Console → Functions → subscriptionsOnRcPremiumUpdate',
|
|
426
433
|
|
|
@@ -488,6 +495,14 @@ module.exports = {
|
|
|
488
495
|
'new.firebase.q.revenuecat.webhookKey.hint': 'Save this value. In RevenueCat dashboard, paste as: Bearer <this-value>',
|
|
489
496
|
'new.firebase.q.revenuecat.metaToken': 'Meta Access Token (for Ads Conversions API, optional)',
|
|
490
497
|
'new.firebase.q.revenuecat.metaDataset': 'Meta Dataset ID / Pixel ID (optional)',
|
|
498
|
+
'new.firebase.q.revenuecat.test': 'Test Store key (test_xxx) — optional, works for both iOS+Android and runs in the simulator',
|
|
499
|
+
'new.firebase.q.revenuecat.test.invalid': 'Test Store key must start with test_ (e.g. test_xxxxxxxxxxxxxxxxxxxx).',
|
|
500
|
+
'new.firebase.q.revenuecat.iosProd': 'iOS production key (appl_xxx) — optional, used only on physical iPhone',
|
|
501
|
+
'new.firebase.q.revenuecat.iosProd.invalid': 'iOS production key must start with appl_ (e.g. appl_xxxxxxxxxxxxxxx).',
|
|
502
|
+
'new.firebase.q.revenuecat.androidProd': 'Android production key (goog_xxx) — optional, used only on physical Android',
|
|
503
|
+
'new.firebase.q.revenuecat.androidProd.invalid': 'Android production key must start with goog_ (e.g. goog_xxxxxxxxxxxxxxx).',
|
|
504
|
+
'new.firebase.q.revenuecat.atLeastOne': 'Configure at least one key (Test, iOS prod or Android prod). You can add the others later in .env.',
|
|
505
|
+
// Legacy keys — kept for projects/scripts that still reference them.
|
|
491
506
|
'new.firebase.q.revenuecat.android': 'RevenueCat API key for Android',
|
|
492
507
|
'new.firebase.q.revenuecat.ios': 'RevenueCat API key for iOS',
|
|
493
508
|
'new.firebase.q.paywall': 'Which paywall style?',
|
|
@@ -661,6 +676,12 @@ module.exports = {
|
|
|
661
676
|
'run.stage.buildSuccess': 'Build done — launching app…',
|
|
662
677
|
'run.error.notFlutterProject': 'No pubspec.yaml found. Run this command from inside a Flutter project.',
|
|
663
678
|
'run.error.flutterNotFound': 'Flutter not found. Make sure Flutter is installed and on your PATH.',
|
|
679
|
+
'run.rc.usingTest': 'RevenueCat: using test key (test_) — simulator/emulator',
|
|
680
|
+
'run.rc.usingProd': 'RevenueCat: using production keys — physical device',
|
|
681
|
+
'run.rc.fallbackToTest': 'RevenueCat: production key missing ({platform}) — falling back to test_; set RC_{var} in .env to test real in-app purchases',
|
|
682
|
+
'run.rc.forcedTest': 'RevenueCat: --rc=test forced',
|
|
683
|
+
'run.rc.forcedProd': 'RevenueCat: --rc=prod forced',
|
|
684
|
+
'run.rc.forcedProdMissing': 'RevenueCat: --rc=prod requested but RC_IOS_PROD_KEY/RC_ANDROID_PROD_KEY not set in .env',
|
|
664
685
|
|
|
665
686
|
// reset command
|
|
666
687
|
'cli.command.reset.description': 'Uninstall the app on a simulator/emulator/device so you can test as a fresh install',
|
|
@@ -840,6 +861,10 @@ module.exports = {
|
|
|
840
861
|
'add.cancelled': 'Cancelled.',
|
|
841
862
|
'add.prompt.sentryDsn': 'Sentry DSN (leave blank to configure later):',
|
|
842
863
|
'add.prompt.mixpanelToken': 'Mixpanel Token (leave blank to configure later):',
|
|
864
|
+
'add.prompt.rcTestKey': 'Test Store key (test_xxx) — optional, works for both iOS+Android and on simulator:',
|
|
865
|
+
'add.prompt.rcIosProdKey': 'iOS production key (appl_xxx) — optional, only on physical device:',
|
|
866
|
+
'add.prompt.rcAndroidProdKey': 'Android production key (goog_xxx) — optional, only on physical device:',
|
|
867
|
+
// Legacy keys — kept for compatibility with old scripts.
|
|
843
868
|
'add.prompt.rcAndroidKey': 'RevenueCat Android API key (leave blank to configure later):',
|
|
844
869
|
'add.prompt.rcIosKey': 'RevenueCat iOS API key (leave blank to configure later):',
|
|
845
870
|
'add.note.facebook': 'Add your Facebook App ID and token in .vscode/launch.json (FB_APP_ID, FB_TOKEN).',
|
|
@@ -421,8 +421,15 @@ module.exports = {
|
|
|
421
421
|
|
|
422
422
|
'doctor.revenuecat.title': 'RevenueCat',
|
|
423
423
|
'doctor.revenuecat.keysOk': 'Claves de API configuradas (iOS + Android)',
|
|
424
|
-
'doctor.revenuecat.keysEmpty': '
|
|
425
|
-
'doctor.revenuecat.
|
|
424
|
+
'doctor.revenuecat.keysEmpty': 'Ninguna clave configurada — define al menos RC_TEST_KEY en .env (kasy run la usa en simulador/emulador)',
|
|
425
|
+
'doctor.revenuecat.testKeyOk': 'RC_TEST_KEY configurada (test_) — usada en simulador/emulador',
|
|
426
|
+
'doctor.revenuecat.testKeyMissing': 'RC_TEST_KEY ausente — el flujo de suscripción no funciona en simulador/emulador',
|
|
427
|
+
'doctor.revenuecat.iosProdOk': 'RC_IOS_PROD_KEY configurada (appl_) — usada en iPhone físico',
|
|
428
|
+
'doctor.revenuecat.iosProdMissing': 'RC_IOS_PROD_KEY ausente — kasy run en iPhone físico va a usar la clave de prueba',
|
|
429
|
+
'doctor.revenuecat.androidProdOk': 'RC_ANDROID_PROD_KEY configurada (goog_) — usada en Android físico',
|
|
430
|
+
'doctor.revenuecat.androidProdMissing': 'RC_ANDROID_PROD_KEY ausente — kasy run en Android físico va a usar la clave de prueba',
|
|
431
|
+
'doctor.revenuecat.prefixMismatch': 'Clave con prefijo incorrecto: {key} debe empezar con {expected}',
|
|
432
|
+
'doctor.revenuecat.keysTest': 'Solo claves Test Store (test_) configuradas — los releases en la tienda requieren appl_/goog_, sino el app crashea',
|
|
426
433
|
'doctor.revenuecat.webhookUrlSupabase': 'URL del webhook (pega en RevenueCat → Integrations → Webhooks)',
|
|
427
434
|
'doctor.revenuecat.webhookUrlFirebase': 'URL del webhook: Firebase Console → Functions → subscriptionsOnRcPremiumUpdate',
|
|
428
435
|
|
|
@@ -488,6 +495,14 @@ module.exports = {
|
|
|
488
495
|
'new.firebase.q.revenuecat.webhookKey.hint': 'Guarda este valor. En el panel RevenueCat, pega como: Bearer <este-valor>',
|
|
489
496
|
'new.firebase.q.revenuecat.metaToken': 'Meta Access Token (Conversions API, opcional)',
|
|
490
497
|
'new.firebase.q.revenuecat.metaDataset': 'Meta Dataset ID / Pixel ID (opcional)',
|
|
498
|
+
'new.firebase.q.revenuecat.test': 'Clave Test Store (test_xxx) — opcional, sirve para iOS+Android y funciona en simulador',
|
|
499
|
+
'new.firebase.q.revenuecat.test.invalid': 'Clave Test Store debe empezar con test_ (ej: test_xxxxxxxxxxxxxxxxxxxx).',
|
|
500
|
+
'new.firebase.q.revenuecat.iosProd': 'Clave iOS de producción (appl_xxx) — opcional, se usa solo en dispositivo físico',
|
|
501
|
+
'new.firebase.q.revenuecat.iosProd.invalid': 'Clave iOS de producción debe empezar con appl_ (ej: appl_xxxxxxxxxxxxxxx).',
|
|
502
|
+
'new.firebase.q.revenuecat.androidProd': 'Clave Android de producción (goog_xxx) — opcional, se usa solo en dispositivo físico',
|
|
503
|
+
'new.firebase.q.revenuecat.androidProd.invalid': 'Clave Android de producción debe empezar con goog_ (ej: goog_xxxxxxxxxxxxxxx).',
|
|
504
|
+
'new.firebase.q.revenuecat.atLeastOne': 'Configura al menos una clave (Test, iOS prod o Android prod). Puedes agregar las demás después en .env.',
|
|
505
|
+
// Legacy keys — kept for projects/scripts that still reference them.
|
|
491
506
|
'new.firebase.q.revenuecat.android': 'Clave API RevenueCat para Android',
|
|
492
507
|
'new.firebase.q.revenuecat.ios': 'Clave API RevenueCat para iOS',
|
|
493
508
|
'new.firebase.q.paywall': '¿Qué estilo de paywall?',
|
|
@@ -694,6 +709,12 @@ module.exports = {
|
|
|
694
709
|
'run.stage.buildSuccess': 'Build listo — abriendo la app…',
|
|
695
710
|
'run.error.notFlutterProject': 'No se encontro pubspec.yaml. Ejecuta este comando dentro de un proyecto Flutter.',
|
|
696
711
|
'run.error.flutterNotFound': 'Flutter no encontrado. Verifica que Flutter este instalado y en el PATH.',
|
|
712
|
+
'run.rc.usingTest': 'RevenueCat: usando clave de prueba (test_) — simulador/emulador',
|
|
713
|
+
'run.rc.usingProd': 'RevenueCat: usando claves de producción — dispositivo físico',
|
|
714
|
+
'run.rc.fallbackToTest': 'RevenueCat: clave de producción ausente ({platform}) — usando test_; configura RC_{var} en .env para probar in-app purchase real',
|
|
715
|
+
'run.rc.forcedTest': 'RevenueCat: --rc=test forzado',
|
|
716
|
+
'run.rc.forcedProd': 'RevenueCat: --rc=prod forzado',
|
|
717
|
+
'run.rc.forcedProdMissing': 'RevenueCat: --rc=prod pedido pero RC_IOS_PROD_KEY/RC_ANDROID_PROD_KEY no configuradas en .env',
|
|
697
718
|
|
|
698
719
|
// doctor project checks
|
|
699
720
|
'doctor.project.title': 'Proyecto',
|
|
@@ -838,6 +859,10 @@ module.exports = {
|
|
|
838
859
|
'add.cancelled': 'Cancelado.',
|
|
839
860
|
'add.prompt.sentryDsn': 'Sentry DSN (deja en blanco para configurar después):',
|
|
840
861
|
'add.prompt.mixpanelToken': 'Mixpanel Token (deja en blanco para configurar después):',
|
|
862
|
+
'add.prompt.rcTestKey': 'Clave Test Store (test_xxx) — opcional, sirve para iOS+Android y simulador:',
|
|
863
|
+
'add.prompt.rcIosProdKey': 'Clave iOS de producción (appl_xxx) — opcional, solo en dispositivo físico:',
|
|
864
|
+
'add.prompt.rcAndroidProdKey': 'Clave Android de producción (goog_xxx) — opcional, solo en dispositivo físico:',
|
|
865
|
+
// Legacy keys — kept for compatibility with old scripts.
|
|
841
866
|
'add.prompt.rcAndroidKey': 'RevenueCat Android API key (deja en blanco para configurar después):',
|
|
842
867
|
'add.prompt.rcIosKey': 'RevenueCat iOS API key (deja en blanco para configurar después):',
|
|
843
868
|
'add.note.facebook': 'Agrega tu Facebook App ID y token en .vscode/launch.json (FB_APP_ID, FB_TOKEN).',
|
|
@@ -419,8 +419,15 @@ module.exports = {
|
|
|
419
419
|
|
|
420
420
|
'doctor.revenuecat.title': 'RevenueCat',
|
|
421
421
|
'doctor.revenuecat.keysOk': 'Chaves de API configuradas (iOS + Android)',
|
|
422
|
-
'doctor.revenuecat.keysEmpty': '
|
|
423
|
-
'doctor.revenuecat.
|
|
422
|
+
'doctor.revenuecat.keysEmpty': 'Nenhuma chave configurada — defina pelo menos RC_TEST_KEY no .env (kasy run usa em simulador/emulador)',
|
|
423
|
+
'doctor.revenuecat.testKeyOk': 'RC_TEST_KEY configurada (test_) — usada em simulador/emulador',
|
|
424
|
+
'doctor.revenuecat.testKeyMissing': 'RC_TEST_KEY ausente — fluxo de assinatura não funciona em simulador/emulador',
|
|
425
|
+
'doctor.revenuecat.iosProdOk': 'RC_IOS_PROD_KEY configurada (appl_) — usada em iPhone físico',
|
|
426
|
+
'doctor.revenuecat.iosProdMissing': 'RC_IOS_PROD_KEY ausente — kasy run em iPhone físico vai usar a chave de teste',
|
|
427
|
+
'doctor.revenuecat.androidProdOk': 'RC_ANDROID_PROD_KEY configurada (goog_) — usada em Android físico',
|
|
428
|
+
'doctor.revenuecat.androidProdMissing': 'RC_ANDROID_PROD_KEY ausente — kasy run em Android físico vai usar a chave de teste',
|
|
429
|
+
'doctor.revenuecat.prefixMismatch': 'Chave com prefixo errado: {key} deveria começar com {expected}',
|
|
430
|
+
'doctor.revenuecat.keysTest': 'Apenas chaves Test Store (test_) configuradas — releases na loja exigem appl_/goog_, senão o app crasha',
|
|
424
431
|
'doctor.revenuecat.webhookUrlSupabase': 'URL do webhook (cole no RevenueCat → Integrations → Webhooks)',
|
|
425
432
|
'doctor.revenuecat.webhookUrlFirebase': 'URL do webhook: Firebase Console → Functions → subscriptionsOnRcPremiumUpdate',
|
|
426
433
|
|
|
@@ -488,6 +495,14 @@ module.exports = {
|
|
|
488
495
|
'new.firebase.q.revenuecat.webhookKey.hint': 'Salve esse valor. No painel RevenueCat, cole como: Bearer <esse-valor>',
|
|
489
496
|
'new.firebase.q.revenuecat.metaToken': 'Meta Access Token (Conversions API, opcional)',
|
|
490
497
|
'new.firebase.q.revenuecat.metaDataset': 'Meta Dataset ID / Pixel ID (opcional)',
|
|
498
|
+
'new.firebase.q.revenuecat.test': 'Chave Test Store (test_xxx) — opcional, serve pra iOS+Android e funciona em simulador',
|
|
499
|
+
'new.firebase.q.revenuecat.test.invalid': 'Chave Test Store deve começar com test_ (ex: test_xxxxxxxxxxxxxxxxxxxx).',
|
|
500
|
+
'new.firebase.q.revenuecat.iosProd': 'Chave iOS de produção (appl_xxx) — opcional, usada só em dispositivo físico',
|
|
501
|
+
'new.firebase.q.revenuecat.iosProd.invalid': 'Chave iOS de produção deve começar com appl_ (ex: appl_xxxxxxxxxxxxxxx).',
|
|
502
|
+
'new.firebase.q.revenuecat.androidProd': 'Chave Android de produção (goog_xxx) — opcional, usada só em dispositivo físico',
|
|
503
|
+
'new.firebase.q.revenuecat.androidProd.invalid': 'Chave Android de produção deve começar com goog_ (ex: goog_xxxxxxxxxxxxxxx).',
|
|
504
|
+
'new.firebase.q.revenuecat.atLeastOne': 'Configure pelo menos uma chave (Test, iOS prod ou Android prod). Você pode adicionar as outras depois no .env.',
|
|
505
|
+
// Legacy keys — kept for projects/scripts that still reference them.
|
|
491
506
|
'new.firebase.q.revenuecat.android': 'Chave de API RevenueCat para Android',
|
|
492
507
|
'new.firebase.q.revenuecat.ios': 'Chave de API RevenueCat para iOS',
|
|
493
508
|
'new.firebase.q.paywall': 'Qual estilo de paywall?',
|
|
@@ -694,6 +709,12 @@ module.exports = {
|
|
|
694
709
|
'run.stage.buildSuccess': 'Build pronto — abrindo o app…',
|
|
695
710
|
'run.error.notFlutterProject': 'Nenhum pubspec.yaml encontrado. Execute este comando dentro de um projeto Flutter.',
|
|
696
711
|
'run.error.flutterNotFound': 'Flutter não encontrado. Verifique se o Flutter está instalado e no PATH.',
|
|
712
|
+
'run.rc.usingTest': 'RevenueCat: usando chave de teste (test_) — simulador/emulador',
|
|
713
|
+
'run.rc.usingProd': 'RevenueCat: usando chaves de produção — dispositivo físico',
|
|
714
|
+
'run.rc.fallbackToTest': 'RevenueCat: chave de produção ausente ({platform}) — caindo para test_; configure RC_{var} no .env para testar in-app purchase real',
|
|
715
|
+
'run.rc.forcedTest': 'RevenueCat: --rc=test forçado',
|
|
716
|
+
'run.rc.forcedProd': 'RevenueCat: --rc=prod forçado',
|
|
717
|
+
'run.rc.forcedProdMissing': 'RevenueCat: --rc=prod pedido mas RC_IOS_PROD_KEY/RC_ANDROID_PROD_KEY não configuradas no .env',
|
|
697
718
|
|
|
698
719
|
// doctor project checks
|
|
699
720
|
'doctor.project.title': 'Projeto',
|
|
@@ -838,6 +859,10 @@ module.exports = {
|
|
|
838
859
|
'add.cancelled': 'Cancelado.',
|
|
839
860
|
'add.prompt.sentryDsn': 'Sentry DSN (deixe em branco para configurar depois):',
|
|
840
861
|
'add.prompt.mixpanelToken': 'Mixpanel Token (deixe em branco para configurar depois):',
|
|
862
|
+
'add.prompt.rcTestKey': 'Chave Test Store (test_xxx) — opcional, serve pra iOS+Android e simulador:',
|
|
863
|
+
'add.prompt.rcIosProdKey': 'Chave iOS de produção (appl_xxx) — opcional, só em dispositivo físico:',
|
|
864
|
+
'add.prompt.rcAndroidProdKey': 'Chave Android de produção (goog_xxx) — opcional, só em dispositivo físico:',
|
|
865
|
+
// Legacy keys — kept for compatibility with old scripts.
|
|
841
866
|
'add.prompt.rcAndroidKey': 'RevenueCat Android API key (deixe em branco para configurar depois):',
|
|
842
867
|
'add.prompt.rcIosKey': 'RevenueCat iOS API key (deixe em branco para configurar depois):',
|
|
843
868
|
'add.note.facebook': 'Adicione seu Facebook App ID e token no .vscode/launch.json (FB_APP_ID, FB_TOKEN).',
|
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
|
}
|