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.
Files changed (27) hide show
  1. package/bin/kasy.js +1 -0
  2. package/lib/commands/add.js +45 -12
  3. package/lib/commands/doctor.js +37 -6
  4. package/lib/commands/new.js +34 -8
  5. package/lib/commands/remove.js +14 -3
  6. package/lib/commands/run.js +207 -5
  7. package/lib/scaffold/CHANGELOG.json +9 -0
  8. package/lib/scaffold/backends/api/patch/README.md +3 -2
  9. package/lib/scaffold/backends/supabase/patch/README.md +3 -2
  10. package/lib/scaffold/shared/generator-utils.js +52 -8
  11. package/lib/scaffold/shared/post-build.js +105 -31
  12. package/lib/scaffold/shared/template-strings.js +6 -0
  13. package/lib/utils/i18n/messages-en.js +27 -2
  14. package/lib/utils/i18n/messages-es.js +27 -2
  15. package/lib/utils/i18n/messages-pt.js +27 -2
  16. package/package.json +1 -1
  17. package/templates/firebase/README.en.md +17 -7
  18. package/templates/firebase/README.es.md +17 -7
  19. package/templates/firebase/README.md +17 -7
  20. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +15 -0
  21. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +3 -19
  22. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidgetReceiver.kt +37 -0
  23. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/OpenAppAction.kt +26 -0
  24. package/templates/firebase/docs/revenuecat-setup.es.md +28 -8
  25. package/templates/firebase/docs/revenuecat-setup.pt.md +28 -8
  26. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +39 -41
  27. package/templates/firebase/lib/router.dart +15 -1
@@ -665,49 +665,123 @@ async function validateFacebookAndroidStrings(projectDir) {
665
665
  }
666
666
 
667
667
  /**
668
- * Validate RevenueCat API keys (RC_IOS_API_KEY, RC_ANDROID_API_KEY) from the project Makefile
669
- * and return the webhook URL derived from kit_setup.json backend config.
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 validateRevenueCat(projectDir, config = {}) {
672
- const makefilePath = path.join(projectDir, 'Makefile');
673
- if (!(await fs.pathExists(makefilePath))) {
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(makefilePath, 'utf8');
680
- } catch (err) {
681
- return { ok: false, error: `Failed to read Makefile: ${err.message}` };
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
- // Only consider non-commented lines
685
- const activeLines = content.split('\n').filter((l) => !/^\s*#/.test(l)).join('\n');
686
-
687
- const iosMatch = activeLines.match(/RC_IOS_API_KEY=([^\s\\]+)/);
688
- const androidMatch = activeLines.match(/RC_ANDROID_API_KEY=([^\s\\]+)/);
689
-
690
- const iosKey = iosMatch ? iosMatch[1].trim() : '';
691
- const androidKey = androidMatch ? androidMatch[1].trim() : '';
692
-
693
- const iosEmpty = !iosKey || iosKey === 'xxx';
694
- const androidEmpty = !androidKey || androidKey === 'xxx';
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: !iosEmpty && !androidEmpty,
706
- iosKey,
707
- androidKey,
708
- iosEmpty,
709
- androidEmpty,
710
- bothTest: iosTest && androidTest,
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': 'API keys not configured — set RC_IOS_API_KEY and RC_ANDROID_API_KEY in Makefile and .vscode/launch.json',
423
- 'doctor.revenuecat.keysTest': 'Using Test Store keys (test_) — replace with appl_ and goog_ for production',
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': 'Claves de API no configuradas — define RC_IOS_API_KEY y RC_ANDROID_API_KEY en Makefile y .vscode/launch.json',
425
- 'doctor.revenuecat.keysTest': 'Usando claves Test Store (test_) — reemplaza por appl_ y goog_ para producción',
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': 'Chaves de API não configuradas — defina RC_IOS_API_KEY e RC_ANDROID_API_KEY no Makefile e .vscode/launch.json',
423
- 'doctor.revenuecat.keysTest': 'Usando chaves Test Store (test_) — substitua por appl_ e goog_ para produção',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -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
- | `RC_ANDROID_API_KEY` | RevenueCat | RevenueCat dashboard → Apps → Android**Google Play** public key (`goog_…`) |
52
- | `RC_IOS_API_KEY` | RevenueCat | RevenueCat dashboard → Apps → iOS → **Apple App Store** public key (`appl_…`) |
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: `test_` vs store keys (TestFlight / release)
56
+ ### RevenueCat: `kasy run` picks the right key automatically
56
57
 
57
- - Keys starting with **`test_`** are RevenueCat **Test Store**: fine for **`flutter run` debug** on your Mac (fake purchases in RC’s test environment). **Do not** bake them into TestFlight/App Store IPAs — the SDK rejects them or the kit skips RC init.
58
- - **TestFlight / Apple sandbox** (no real charges): use **store** public keys — **`appl_…`** (iOS) and **`goog_…`** (Android). TestFlight purchases still use **Apple/Google sandbox**, not real billing.
59
- - The VS Code **Firebase prod** config **no longer** sets RC keys; put `appl_` / `goog_` in **`Firebase/.env`** or **`.dart_defines`** (see `Makefile`) when building release / TestFlight.
60
- - **Firebase — dev** still ships sample `test_` keys for local development only.
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
- | `RC_ANDROID_API_KEY` | RevenueCat | Panel RevenueCat → Apps → Android → clave pública **Google Play** (`goog_…`) |
52
- | `RC_IOS_API_KEY` | RevenueCat | Panel RevenueCat → Apps → iOS → clave pública **Apple App Store** (`appl_…`) |
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: `test_` vs tienda (TestFlight / release)
56
+ ### RevenueCat: `kasy run` elige la clave automáticamente
56
57
 
57
- - Clave **`test_`** = **Test Store** de RevenueCat: sirve para **`flutter run` en debug** en tu Mac. **No** la empaques en IPA de TestFlight/App Store.
58
- - **TestFlight / sandbox de Apple** (sin cobro real): usa claves de **tienda** — **`appl_…`** (iOS) y **`goog_…`** (Android).
59
- - La config **Firebase prod** en VS Code **ya no** define RC; pon `appl_` / `goog_` en **`.env`** o **`.dart_defines`** al generar release / TestFlight.
60
- - **Firebase — dev** sigue con `test_` de ejemplo solo para desarrollo local.
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
- | `RC_ANDROID_API_KEY` | RevenueCat | Dashboard RevenueCat → Apps → Android → chave pública **Google Play** (`goog_…`) |
52
- | `RC_IOS_API_KEY` | RevenueCat | Dashboard RevenueCat → Apps → iOS → chave pública **Apple App Store** (`appl_…`) |
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: `test_` vs loja (TestFlight / release)
56
+ ### RevenueCat: `kasy run` escolhe a chave automaticamente
56
57
 
57
- - Chave que começa com **`test_`** = **Test Store** da RevenueCat: serve para **`flutter run` em debug** no Mac (compras fictícias no ambiente de teste da RC). **Não** use em IPA de TestFlight/App Store — o SDK bloqueia ou o app ignora o RC (comportamento atual do kit).
58
- - **TestFlight / sandbox da Apple** (compras sem cobrar de verdade): use no IPA as chaves públicas de **loja** — **`appl_…`** (iOS) e **`goog_…`** (Android). No TestFlight as compras ainda são **sandbox** da Apple/Google, não cobrança real.
59
- - A configuração **Firebase prod** no VS Code **não** define mais RC; coloque `appl_` / `goog_` no seu **`.env`** na raiz do `Firebase/` ou em **`.dart_defines`** (ver `Makefile`) ao gerar release / TestFlight.
60
- - **Firebase — dev** no VS Code continua com `test_` de exemplo só para desenvolvimento local.
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
 
@@ -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.actionStartActivity
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(actionStartActivity(launchAppIntent(context))),
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(actionStartActivity(launchAppIntent(context))),
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
  }
@@ -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
  }