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
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
|
+
}
|
|
@@ -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
|
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import 'dart:async';
|
|
2
|
-
import 'dart:io' show Platform;
|
|
3
|
-
|
|
4
|
-
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
5
1
|
import 'package:home_widget/home_widget.dart';
|
|
6
2
|
import 'package:kasy_kit/core/data/models/user.dart';
|
|
7
3
|
import 'package:kasy_kit/core/home_widgets/home_widget_service.dart';
|
|
@@ -38,12 +34,6 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
38
34
|
/// Snapshot of the user fields the widget reads. Used to skip the update
|
|
39
35
|
/// when an unrelated field changes (e.g. lastUpdateDate refresh).
|
|
40
36
|
(String?, String?, String?, bool) _widgetSignature(User user) {
|
|
41
|
-
final isPro = switch (user) {
|
|
42
|
-
AuthenticatedUserData(:final subscription) ||
|
|
43
|
-
AnonymousUserData(:final subscription) =>
|
|
44
|
-
subscription?.isActive ?? false,
|
|
45
|
-
_ => false,
|
|
46
|
-
};
|
|
47
37
|
final name = switch (user) {
|
|
48
38
|
AuthenticatedUserData(:final name) => name,
|
|
49
39
|
_ => null,
|
|
@@ -52,21 +42,41 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
52
42
|
AuthenticatedUserData(:final email) => email,
|
|
53
43
|
_ => null,
|
|
54
44
|
};
|
|
55
|
-
return (user.idOrNull, name, email,
|
|
45
|
+
return (user.idOrNull, name, email, _cachedIsPro(user));
|
|
56
46
|
}
|
|
57
47
|
|
|
58
48
|
@override
|
|
59
|
-
Future<void> update() =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
///
|
|
65
|
-
///
|
|
66
|
-
|
|
49
|
+
Future<void> update() => _renderForLocale(
|
|
50
|
+
LocaleSettings.currentLocale,
|
|
51
|
+
refreshSubscription: true,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
/// Same as [update] but renders against an explicit locale and skips the
|
|
55
|
+
/// RevenueCat refresh. Two reasons:
|
|
56
|
+
/// 1. `LocaleSettings.setLocale` propagates to `currentLocale` over a
|
|
57
|
+
/// frame boundary, and an [update] call scheduled at the same time
|
|
58
|
+
/// can race with it — passing the locale removes the race.
|
|
59
|
+
/// 2. The network call inside [_resolveIsPro] adds up to 2s of latency
|
|
60
|
+
/// before the new strings reach SharedPreferences, which is what made
|
|
61
|
+
/// the widget look "one step behind" after switching language.
|
|
62
|
+
Future<void> updateForLocale(AppLocale locale) =>
|
|
63
|
+
_renderForLocale(locale, refreshSubscription: false);
|
|
64
|
+
|
|
65
|
+
Future<void> _renderForLocale(
|
|
66
|
+
AppLocale locale, {
|
|
67
|
+
required bool refreshSubscription,
|
|
68
|
+
}) async {
|
|
67
69
|
final logger = Logger();
|
|
68
70
|
logger.i('🔄 Updating MyWidget Home Widget (${locale.languageCode})');
|
|
69
71
|
final user = ref.read(userStateNotifierProvider).user;
|
|
72
|
+
|
|
73
|
+
// Slang lazy-loads non-base locales: AppLocale.pt.translations falls back
|
|
74
|
+
// silently to the base locale (en) until the locale's bundle is loaded
|
|
75
|
+
// into memory. setLocale() kicks off that load asynchronously, so a tap
|
|
76
|
+
// on "Português" immediately followed by updateForLocale(pt) would push
|
|
77
|
+
// English text to the widget on the first try and the correct one on
|
|
78
|
+
// the retry. Awaiting loadLocale here removes the race.
|
|
79
|
+
await LocaleSettings.instance.loadLocale(locale);
|
|
70
80
|
final t = locale.translations;
|
|
71
81
|
|
|
72
82
|
// "Logged out" = no user id at all (post-logout in authRequired mode, or
|
|
@@ -86,7 +96,8 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
86
96
|
_ => null,
|
|
87
97
|
};
|
|
88
98
|
|
|
89
|
-
final isPro = !isLoggedOut &&
|
|
99
|
+
final isPro = !isLoggedOut &&
|
|
100
|
+
(refreshSubscription ? await _resolveIsPro(user) : _cachedIsPro(user));
|
|
90
101
|
|
|
91
102
|
final greeting = _greeting(t);
|
|
92
103
|
final title = isLoggedOut
|
|
@@ -115,18 +126,20 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
115
126
|
});
|
|
116
127
|
}
|
|
117
128
|
|
|
129
|
+
bool _cachedIsPro(User user) => switch (user) {
|
|
130
|
+
AuthenticatedUserData(:final subscription) ||
|
|
131
|
+
AnonymousUserData(:final subscription) =>
|
|
132
|
+
subscription?.isActive ?? false,
|
|
133
|
+
_ => false,
|
|
134
|
+
};
|
|
135
|
+
|
|
118
136
|
/// Returns true if the user has an active subscription.
|
|
119
137
|
/// Queries the SubscriptionRepository directly so the widget reflects the
|
|
120
138
|
/// most recent state — including the RevenueCat fallback when the backend
|
|
121
139
|
/// webhook is delayed. Falls back to the in-memory user.subscription if the
|
|
122
140
|
/// fresh fetch fails (no network, RC not initialised, etc.).
|
|
123
141
|
Future<bool> _resolveIsPro(User user) async {
|
|
124
|
-
final cached =
|
|
125
|
-
AuthenticatedUserData(:final subscription) ||
|
|
126
|
-
AnonymousUserData(:final subscription) =>
|
|
127
|
-
subscription?.isActive ?? false,
|
|
128
|
-
_ => false,
|
|
129
|
-
};
|
|
142
|
+
final cached = _cachedIsPro(user);
|
|
130
143
|
final userId = user.idOrNull;
|
|
131
144
|
if (userId == null) return cached;
|
|
132
145
|
try {
|
|
@@ -152,21 +165,6 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
152
165
|
await HomeWidget.saveWidgetData<String>('isPro', data['isPro'] ?? 'false');
|
|
153
166
|
await HomeWidget.saveWidgetData<String>('quote', data['quote'] ?? '');
|
|
154
167
|
|
|
155
|
-
// On Android, saveWidgetData writes via SharedPreferences.apply() which
|
|
156
|
-
// is asynchronous. Glance's HomeWidgetGlanceStateDefinition reads from
|
|
157
|
-
// the same prefs, but a tight saveWidgetData→updateWidget sequence can
|
|
158
|
-
// race the apply() flush — Glance recomposes with the previous values
|
|
159
|
-
// (most visible right after a locale change). Fire two updates spaced
|
|
160
|
-
// out by 400ms each so even slow devices catch the new state.
|
|
161
|
-
if (!kIsWeb && Platform.isAndroid) {
|
|
162
|
-
await Future<void>.delayed(const Duration(milliseconds: 400));
|
|
163
|
-
await HomeWidget.updateWidget(
|
|
164
|
-
name: _androidWidgetName,
|
|
165
|
-
iOSName: _iosWidgetName,
|
|
166
|
-
);
|
|
167
|
-
await Future<void>.delayed(const Duration(milliseconds: 400));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
168
|
await HomeWidget.updateWidget(
|
|
171
169
|
name: _androidWidgetName,
|
|
172
170
|
iOSName: _iosWidgetName,
|
|
@@ -24,6 +24,7 @@ import 'package:kasy_kit/features/settings/ui/components/admin/admin_routes.dart
|
|
|
24
24
|
import 'package:kasy_kit/features/settings/ui/components/admin/send_push_notification_page.dart';
|
|
25
25
|
import 'package:kasy_kit/features/subscription/ui/component/premium_page_factory.dart';
|
|
26
26
|
import 'package:kasy_kit/features/subscription/ui/premium_page.dart';
|
|
27
|
+
import 'package:logger/logger.dart';
|
|
27
28
|
|
|
28
29
|
final goRouterProvider = Provider<GoRouter>((ref) => generateRouter());
|
|
29
30
|
|
|
@@ -41,7 +42,20 @@ GoRouter generateRouter({
|
|
|
41
42
|
return GoRouter(
|
|
42
43
|
initialLocation: '/',
|
|
43
44
|
navigatorKey: navigatorKey,
|
|
44
|
-
|
|
45
|
+
// Catches unknown routes (e.g. an Android warm-start from the home widget
|
|
46
|
+
// landed on a stale URI) and silently sends the user home instead of
|
|
47
|
+
// surfacing a dead-end "404" page. We log the offending URI so a real
|
|
48
|
+
// misconfigured route doesn't get masked.
|
|
49
|
+
// Note: GoRouter doesn't accept both onException and errorBuilder, so the
|
|
50
|
+
// /404 GoRoute below is what reaches PageNotFound when explicitly navigated.
|
|
51
|
+
onException: (context, state, router) {
|
|
52
|
+
Logger().w(
|
|
53
|
+
'GoRouter caught unknown route → "${state.uri}" '
|
|
54
|
+
'(matched: "${state.matchedLocation}", error: ${state.error}). '
|
|
55
|
+
'Redirecting to "/".',
|
|
56
|
+
);
|
|
57
|
+
router.go('/');
|
|
58
|
+
},
|
|
45
59
|
observers: [
|
|
46
60
|
AnalyticsObserver(analyticsApi: MixpanelAnalyticsApi.instance()),
|
|
47
61
|
|