kasy-cli 1.34.0 → 1.36.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/README.md +1 -1
- package/bin/kasy.js +24 -2
- package/docs/cli-reference.md +7 -7
- package/lib/commands/new.js +11 -9
- package/lib/commands/release-version.js +234 -0
- package/lib/commands/update.js +27 -0
- package/lib/scaffold/CHANGELOG.json +18 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +35 -21
- package/lib/scaffold/backends/patch-base-hashes.json +66 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
- package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +82 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
- package/lib/scaffold/generate.js +53 -4
- package/lib/utils/i18n/messages-en.js +23 -0
- package/lib/utils/i18n/messages-es.js +23 -0
- package/lib/utils/i18n/messages-pt.js +23 -0
- package/package.json +5 -2
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
- package/templates/firebase/AGENTS.md +83 -0
- package/templates/firebase/DESIGN_SYSTEM.md +37 -2
- package/templates/firebase/docs/auth-setup.en.md +2 -0
- package/templates/firebase/docs/auth-setup.es.md +2 -0
- package/templates/firebase/docs/auth-setup.pt.md +2 -0
- package/templates/firebase/firebase.json +56 -1
- package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
- package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
- package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
- package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
- package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
- package/templates/firebase/lib/components/kasy_alert.dart +0 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +31 -16
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
- package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
- package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
- package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
- package/templates/firebase/lib/components/kasy_sidebar.dart +215 -178
- package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
- package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
- package/templates/firebase/lib/components/kasy_toast.dart +107 -41
- package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
- package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
- package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
- package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
- package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
- package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
- package/templates/firebase/lib/core/guards/guard.dart +16 -2
- package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
- package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +5 -3
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
- package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
- package/templates/firebase/lib/core/states/logout_action.dart +5 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
- package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
- package/templates/firebase/lib/core/theme/texts.dart +90 -57
- package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
- package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
- package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
- package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
- package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
- package/templates/firebase/lib/core/web_screen_width.dart +15 -0
- package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
- package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
- package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
- package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +1 -2
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
- package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
- package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +205 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
- package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
- package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
- package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
- package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
- package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
- package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
- package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +59 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
- package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
- package/templates/firebase/lib/features/home/home_components_page.dart +4 -3
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +226 -105
- package/templates/firebase/lib/features/home/home_page.dart +4 -0
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +8 -3
- package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -1
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +8 -3
- package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
- package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
- package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +43 -15
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
- package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
- package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
- package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
- package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
- package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
- package/templates/firebase/lib/i18n/en.i18n.json +49 -3
- package/templates/firebase/lib/i18n/es.i18n.json +49 -3
- package/templates/firebase/lib/i18n/pt.i18n.json +49 -3
- package/templates/firebase/lib/main.dart +11 -2
- package/templates/firebase/lib/router.dart +92 -13
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
- package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
- package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
- package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
- package/templates/firebase/web/index.html +162 -14
- package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ The wizard will ask for your app name, bundle ID, and backend. A complete Flutte
|
|
|
51
51
|
|
|
52
52
|
## Optional modules
|
|
53
53
|
|
|
54
|
-
`sentry` · `analytics` · `revenuecat` · `onboarding` · `widget` · `
|
|
54
|
+
`sentry` · `analytics` · `revenuecat` · `onboarding` · `widget` · `ai_chat` · `facebook` · `local_notifications` · `feedback` · `ci`
|
|
55
55
|
|
|
56
56
|
## Requirements
|
|
57
57
|
|
package/bin/kasy.js
CHANGED
|
@@ -8,6 +8,7 @@ const { runDoctor } = require('../lib/commands/doctor');
|
|
|
8
8
|
const { runFeatures } = require('../lib/commands/features');
|
|
9
9
|
const { runValidate } = require('../lib/commands/validate');
|
|
10
10
|
const { runDeployCommand } = require('../lib/commands/deploy');
|
|
11
|
+
const { runReleaseVersion } = require('../lib/commands/release-version');
|
|
11
12
|
const { runConfigure } = require('../lib/commands/configure');
|
|
12
13
|
const { runAppleWeb } = require('../lib/commands/apple-web');
|
|
13
14
|
const { runFacebook } = require('../lib/commands/facebook');
|
|
@@ -327,6 +328,27 @@ function buildProgram(language) {
|
|
|
327
328
|
t
|
|
328
329
|
);
|
|
329
330
|
|
|
331
|
+
applyLocalizedHelp(
|
|
332
|
+
program
|
|
333
|
+
.command('release-version')
|
|
334
|
+
.argument('[directory]', 'Project folder (default: current directory)', '.')
|
|
335
|
+
.option('-v, --version <version>', 'Version now live in the stores (SemVer, e.g. 1.4.0)')
|
|
336
|
+
.option('-f, --force', 'Make it a forced (blocking) update — sets app_min_version')
|
|
337
|
+
.option('-p, --project <id>', 'Firebase Project ID')
|
|
338
|
+
.option('-y, --yes', 'Skip confirmations (non-interactive / CI)')
|
|
339
|
+
.description(t('cli.command.releaseVersion.description'))
|
|
340
|
+
.action(async (directory, options) => {
|
|
341
|
+
await runReleaseVersion(directory, {
|
|
342
|
+
language,
|
|
343
|
+
version: options.version,
|
|
344
|
+
force: Boolean(options.force),
|
|
345
|
+
project: options.project,
|
|
346
|
+
yes: Boolean(options.yes),
|
|
347
|
+
});
|
|
348
|
+
}),
|
|
349
|
+
t
|
|
350
|
+
);
|
|
351
|
+
|
|
330
352
|
applyLocalizedHelp(
|
|
331
353
|
program
|
|
332
354
|
.command('configure')
|
|
@@ -432,7 +454,7 @@ function buildProgram(language) {
|
|
|
432
454
|
applyLocalizedHelp(
|
|
433
455
|
program
|
|
434
456
|
.command('add')
|
|
435
|
-
.argument('[feature]', 'Feature to add (e.g. sentry, analytics, revenuecat, onboarding,
|
|
457
|
+
.argument('[feature]', 'Feature to add (e.g. sentry, analytics, revenuecat, onboarding, ai_chat, ci...)')
|
|
436
458
|
.option('--list', 'List all available features and their current status')
|
|
437
459
|
.option('--yes', 'Skip interactive prompts (use placeholder values)')
|
|
438
460
|
.option('-d, --directory <path>', 'Project folder (default: current directory)', '.')
|
|
@@ -446,7 +468,7 @@ function buildProgram(language) {
|
|
|
446
468
|
applyLocalizedHelp(
|
|
447
469
|
program
|
|
448
470
|
.command('remove')
|
|
449
|
-
.argument('[feature]', 'Feature to remove (e.g. sentry, analytics,
|
|
471
|
+
.argument('[feature]', 'Feature to remove (e.g. sentry, analytics, ai_chat, ci...)')
|
|
450
472
|
.option('--yes', 'Skip confirmation prompt')
|
|
451
473
|
.option('-d, --directory <path>', 'Project folder (default: current directory)', '.')
|
|
452
474
|
.description(t('cli.command.remove.description'))
|
package/docs/cli-reference.md
CHANGED
|
@@ -17,7 +17,7 @@ kasy new # interativo — pergunta tudo
|
|
|
17
17
|
kasy new meu-app # cria na pasta meu-app
|
|
18
18
|
kasy new --yes # modo rápido, preset Starter
|
|
19
19
|
kasy new --backend supabase # define backend sem perguntar
|
|
20
|
-
kasy new --with
|
|
20
|
+
kasy new --with ai_chat,sentry # pré-seleciona features
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
**Opções:**
|
|
@@ -36,12 +36,12 @@ kasy new --with llm_chat,sentry # pré-seleciona features
|
|
|
36
36
|
Adiciona uma feature a um projeto Kasy existente.
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
kasy add
|
|
39
|
+
kasy add ai_chat
|
|
40
40
|
kasy add sentry
|
|
41
41
|
kasy add revenuecat
|
|
42
42
|
kasy add --list # lista features disponíveis e status
|
|
43
|
-
kasy add --yes
|
|
44
|
-
kasy add -d ./outro-app
|
|
43
|
+
kasy add --yes ai_chat # sem perguntas interativas
|
|
44
|
+
kasy add -d ./outro-app ai_chat
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
**Features disponíveis:**
|
|
@@ -54,7 +54,7 @@ kasy add -d ./outro-app llm_chat
|
|
|
54
54
|
| `onboarding` | Fluxo de onboarding customizável |
|
|
55
55
|
| `web` | Suporte a Flutter Web |
|
|
56
56
|
| `widget` | Home widgets para iOS e Android |
|
|
57
|
-
| `
|
|
57
|
+
| `ai_chat` | Chat com IA via Cloud/Edge Functions |
|
|
58
58
|
| `feedback` | Sistema de feedback e feature requests |
|
|
59
59
|
| `ci` | CI/CD com GitHub Actions / Codemagic |
|
|
60
60
|
|
|
@@ -203,7 +203,7 @@ lib/
|
|
|
203
203
|
home/ # tela principal
|
|
204
204
|
notifications/ # notificações locais e push
|
|
205
205
|
settings/ # configurações do app
|
|
206
|
-
|
|
206
|
+
ai_chat/ # [feature] chat com IA
|
|
207
207
|
feedbacks/ # [feature] feedback e feature requests
|
|
208
208
|
onboarding/ # [feature] onboarding
|
|
209
209
|
subscription/ # [feature] assinaturas (RevenueCat)
|
|
@@ -246,6 +246,6 @@ No desenvolvimento local, ficam em `.env` (copiado de `.env.example`). `--dart-d
|
|
|
246
246
|
| `MIXPANEL_TOKEN` | analytics | Token do Mixpanel |
|
|
247
247
|
| `RC_ANDROID_API_KEY` | revenuecat | Chave Android do RevenueCat |
|
|
248
248
|
| `RC_IOS_API_KEY` | revenuecat | Chave iOS do RevenueCat |
|
|
249
|
-
| `
|
|
249
|
+
| `AI_CHAT_ENDPOINT` | ai_chat | URL da Cloud/Edge Function |
|
|
250
250
|
| `BACKEND_URL` | supabase/api | URL do backend |
|
|
251
251
|
| `SUPABASE_TOKEN` | supabase | Anon key do Supabase |
|
package/lib/commands/new.js
CHANGED
|
@@ -1850,22 +1850,24 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1850
1850
|
// Supabase setup runs in fcmOnly mode, which intentionally leaves Firebase
|
|
1851
1851
|
// Auth untouched, so initialize it here (idempotent) before the deploy.
|
|
1852
1852
|
await ensureFirebaseAuthInitialized(answers.firebaseProjectId);
|
|
1853
|
-
// Web Google sign-in on Supabase brokers the Google ID token through the
|
|
1854
|
-
// Firebase popup (signInWithPopup), which only runs from an authorized
|
|
1855
|
-
// domain. fcmOnly setup skips the authorizedDomains step, so localhost is
|
|
1856
|
-
// missing here and the web popup dies with [firebase_auth/unauthorized-domain].
|
|
1857
|
-
// Add it best-effort now that auth is initialized. Native (mobile) is unaffected.
|
|
1858
|
-
const localhostDomains = await authorizeLocalhostForProject(answers.firebaseProjectId);
|
|
1859
|
-
if (!localhostDomains.ok) {
|
|
1860
|
-
ui.log.warn(tr('new.google.localhostDomainWarn'));
|
|
1861
|
-
}
|
|
1862
1853
|
const cliResult = await enableAuthViaFirebaseCli({
|
|
1863
1854
|
projectDir: targetDir,
|
|
1864
1855
|
projectId: answers.firebaseProjectId,
|
|
1865
1856
|
appName: answers.appName,
|
|
1866
1857
|
googleOnly: true,
|
|
1867
1858
|
});
|
|
1859
|
+
// Web Google sign-in on Supabase brokers the Google ID token through the
|
|
1860
|
+
// Firebase popup (signInWithPopup), which only runs from an authorized
|
|
1861
|
+
// domain. New projects don't get `localhost` seeded automatically, so the web
|
|
1862
|
+
// popup would die with [firebase_auth/unauthorized-domain]. Authorize it now —
|
|
1863
|
+
// AFTER the deploy, which fully materializes and stabilizes the auth config.
|
|
1864
|
+
// Doing it right after initializeAuth raced the config's propagation and
|
|
1865
|
+
// intermittently failed. Best-effort + retries. Native (mobile) is unaffected.
|
|
1866
|
+
const localhostDomains = await authorizeLocalhostForProject(answers.firebaseProjectId);
|
|
1868
1867
|
googleSpinner.stop(tr('new.google.enabling'));
|
|
1868
|
+
if (!localhostDomains.ok) {
|
|
1869
|
+
ui.log.warn(tr('new.google.localhostDomainWarn'));
|
|
1870
|
+
}
|
|
1869
1871
|
|
|
1870
1872
|
if (cliResult.ok) {
|
|
1871
1873
|
// The deploy created the OAuth Web Client + iOS client. Re-run flutterfire so
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kasy release-version` — announce a new app version to existing users.
|
|
3
|
+
*
|
|
4
|
+
* The "update available" prompt in the app compares the installed version
|
|
5
|
+
* (read automatically from pubspec.yaml) against two Firebase Remote Config
|
|
6
|
+
* keys:
|
|
7
|
+
*
|
|
8
|
+
* app_latest_version — newer version published to the stores → OPTIONAL update
|
|
9
|
+
* app_min_version — oldest version still allowed → FORCED update
|
|
10
|
+
*
|
|
11
|
+
* Updating those keys is normally a manual step in the Firebase console (on
|
|
12
|
+
* purpose: publishing to the store and *announcing* the update are different
|
|
13
|
+
* moments). This command automates the announcing part. It reuses the existing
|
|
14
|
+
* Firebase CLI login (`firebase login`) — no service account key, nothing billed.
|
|
15
|
+
*
|
|
16
|
+
* Safety: it bumps `app_latest_version` (optional) by default. Forcing an update
|
|
17
|
+
* (`app_min_version`) is opt-in via `--force`, because a wrong value locks every
|
|
18
|
+
* user out of the app with no way to dismiss.
|
|
19
|
+
*
|
|
20
|
+
* It never overwrites the rest of your Remote Config: it fetches the live
|
|
21
|
+
* template, edits only the version keys, and re-publishes.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
const fs = require('fs-extra');
|
|
28
|
+
const kleur = require('kleur');
|
|
29
|
+
const { exec } = require('node:child_process');
|
|
30
|
+
const { promisify } = require('node:util');
|
|
31
|
+
|
|
32
|
+
const ui = require('../utils/ui');
|
|
33
|
+
const { printCompactHeader } = require('../utils/brand');
|
|
34
|
+
const { createTranslator } = require('../utils/i18n');
|
|
35
|
+
const { getStoredLanguage } = require('../utils/license');
|
|
36
|
+
|
|
37
|
+
const execAsync = promisify(exec);
|
|
38
|
+
|
|
39
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
40
|
+
const TEMPLATE_FILE = 'remoteconfig.template.json';
|
|
41
|
+
|
|
42
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/** Semantic version from `version: 1.2.3+45` (drops the build number). */
|
|
45
|
+
function readPubspecVersion(content) {
|
|
46
|
+
const match = content.match(/^version:\s*(\d+\.\d+\.\d+)\+?\d*\s*$/m);
|
|
47
|
+
return match ? match[1] : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Firebase project id — google-services.json is always present; .firebaserc as fallback. */
|
|
51
|
+
async function detectProjectId(projectDir) {
|
|
52
|
+
const gsPath = path.join(projectDir, 'android', 'app', 'google-services.json');
|
|
53
|
+
if (await fs.pathExists(gsPath)) {
|
|
54
|
+
try {
|
|
55
|
+
const data = await fs.readJson(gsPath);
|
|
56
|
+
if (data?.project_info?.project_id) return data.project_info.project_id;
|
|
57
|
+
} catch (_) {}
|
|
58
|
+
}
|
|
59
|
+
const rcPath = path.join(projectDir, '.firebaserc');
|
|
60
|
+
if (await fs.pathExists(rcPath)) {
|
|
61
|
+
try {
|
|
62
|
+
const rc = await fs.readJson(rcPath);
|
|
63
|
+
if (rc.projects?.default) return rc.projects.default;
|
|
64
|
+
} catch (_) {}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function runFirebase(cmd, projectDir) {
|
|
70
|
+
try {
|
|
71
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
72
|
+
cwd: projectDir,
|
|
73
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
74
|
+
});
|
|
75
|
+
return { ok: true, stdout, stderr };
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return { ok: false, error: (err.stderr || err.message || '').trim() };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Set one string parameter, preserving any existing description / metadata. */
|
|
82
|
+
function setStringParam(template, key, value) {
|
|
83
|
+
template.parameters = template.parameters || {};
|
|
84
|
+
const existing = template.parameters[key] || {};
|
|
85
|
+
template.parameters[key] = {
|
|
86
|
+
...existing,
|
|
87
|
+
defaultValue: { value },
|
|
88
|
+
valueType: 'STRING',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Ensure firebase.json points the remoteconfig deploy at our template file.
|
|
94
|
+
* Works on every backend: Supabase / API projects may not ship a firebase.json
|
|
95
|
+
* (they use Firebase Remote Config but don't deploy functions), so we create a
|
|
96
|
+
* minimal one rather than failing.
|
|
97
|
+
*/
|
|
98
|
+
async function ensureFirebaseJsonWired(projectDir) {
|
|
99
|
+
const fbPath = path.join(projectDir, 'firebase.json');
|
|
100
|
+
const fb = (await fs.pathExists(fbPath)) ? await fs.readJson(fbPath) : {};
|
|
101
|
+
if (fb.remoteconfig && fb.remoteconfig.template === TEMPLATE_FILE) return false;
|
|
102
|
+
fb.remoteconfig = { template: TEMPLATE_FILE };
|
|
103
|
+
await fs.writeJson(fbPath, fb, { spaces: 2 });
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Main ──────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
async function runReleaseVersion(directory, options = {}) {
|
|
110
|
+
const language = options.language || getStoredLanguage() || 'en';
|
|
111
|
+
const t = createTranslator(language);
|
|
112
|
+
const projectDir = path.resolve(directory || '.');
|
|
113
|
+
const cancel = () => { ui.cancel(t('releaseVersion.aborted')); process.exit(0); };
|
|
114
|
+
|
|
115
|
+
printCompactHeader(t);
|
|
116
|
+
ui.intro(kleur.bold(t('releaseVersion.intro')));
|
|
117
|
+
|
|
118
|
+
// 1. Validate project
|
|
119
|
+
if (!(await fs.pathExists(path.join(projectDir, 'kit_setup.json')))) {
|
|
120
|
+
ui.log.error(t('releaseVersion.notKasy'));
|
|
121
|
+
ui.cancel(t('releaseVersion.aborted'));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. Installed version (suggestion)
|
|
126
|
+
let installed = null;
|
|
127
|
+
const pubspecPath = path.join(projectDir, 'pubspec.yaml');
|
|
128
|
+
if (await fs.pathExists(pubspecPath)) {
|
|
129
|
+
installed = readPubspecVersion(await fs.readFile(pubspecPath, 'utf8'));
|
|
130
|
+
}
|
|
131
|
+
if (installed) {
|
|
132
|
+
ui.log.message(kleur.gray(t('releaseVersion.current', { version: installed })));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 3. Firebase project
|
|
136
|
+
const projectId = options.project || (await detectProjectId(projectDir));
|
|
137
|
+
if (!projectId) {
|
|
138
|
+
ui.log.error(t('releaseVersion.noProject'));
|
|
139
|
+
ui.cancel(t('releaseVersion.aborted'));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
ui.log.message(kleur.gray(t('releaseVersion.detectedProject', { id: projectId })));
|
|
143
|
+
|
|
144
|
+
// 4. Which version to announce
|
|
145
|
+
let version = options.version;
|
|
146
|
+
if (version && !SEMVER_RE.test(String(version).trim())) {
|
|
147
|
+
ui.log.error(t('releaseVersion.badVersion'));
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
if (!version) {
|
|
151
|
+
version = await ui.text({
|
|
152
|
+
message: t('releaseVersion.qVersion'),
|
|
153
|
+
placeholder: installed || '1.0.0',
|
|
154
|
+
initialValue: installed || '',
|
|
155
|
+
validate: (v) => (SEMVER_RE.test(String(v || '').trim()) ? undefined : t('releaseVersion.badVersion')),
|
|
156
|
+
onCancel: cancel,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
version = String(version).trim();
|
|
160
|
+
|
|
161
|
+
// 5. Optional vs forced (forced is opt-in)
|
|
162
|
+
let forced = Boolean(options.force);
|
|
163
|
+
if (!forced && !options.yes) {
|
|
164
|
+
ui.log.message(kleur.yellow(t('releaseVersion.forcedHint')));
|
|
165
|
+
forced = await ui.confirm({
|
|
166
|
+
message: t('releaseVersion.qForced'),
|
|
167
|
+
initialValue: false,
|
|
168
|
+
onCancel: cancel,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 6. Confirm
|
|
173
|
+
ui.log.message(
|
|
174
|
+
forced
|
|
175
|
+
? kleur.red(t('releaseVersion.summaryForced', { version }))
|
|
176
|
+
: kleur.cyan(t('releaseVersion.summaryOptional', { version }))
|
|
177
|
+
);
|
|
178
|
+
if (!options.yes) {
|
|
179
|
+
const go = await ui.confirm({ message: t('releaseVersion.qConfirm'), onCancel: cancel });
|
|
180
|
+
if (!go) cancel();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 7. Fetch live template, edit only the version keys, re-publish
|
|
184
|
+
const spinner = ui.spinner();
|
|
185
|
+
spinner.start(t('releaseVersion.fetching'));
|
|
186
|
+
|
|
187
|
+
const templatePath = path.join(projectDir, TEMPLATE_FILE);
|
|
188
|
+
const fetched = await runFirebase(
|
|
189
|
+
`firebase remoteconfig:get --project ${projectId} -o "${templatePath}"`,
|
|
190
|
+
projectDir
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
let template;
|
|
194
|
+
if (fetched.ok && (await fs.pathExists(templatePath))) {
|
|
195
|
+
try {
|
|
196
|
+
template = await fs.readJson(templatePath);
|
|
197
|
+
} catch (_) {
|
|
198
|
+
template = {};
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
// First-time / empty config — start fresh (don't abort).
|
|
202
|
+
template = {};
|
|
203
|
+
}
|
|
204
|
+
// Drop server-assigned metadata so the deploy creates a clean new version.
|
|
205
|
+
delete template.version;
|
|
206
|
+
delete template.etag;
|
|
207
|
+
|
|
208
|
+
setStringParam(template, 'app_latest_version', version);
|
|
209
|
+
if (forced) setStringParam(template, 'app_min_version', version);
|
|
210
|
+
|
|
211
|
+
await fs.writeJson(templatePath, template, { spaces: 2 });
|
|
212
|
+
await ensureFirebaseJsonWired(projectDir);
|
|
213
|
+
|
|
214
|
+
spinner.message(t('releaseVersion.deploying'));
|
|
215
|
+
const deployed = await runFirebase(
|
|
216
|
+
`firebase deploy --only remoteconfig --project ${projectId}`,
|
|
217
|
+
projectDir
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (!deployed.ok) {
|
|
221
|
+
spinner.error(t('releaseVersion.failed'));
|
|
222
|
+
ui.log.error(deployed.error);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
spinner.stop(t('releaseVersion.done', { version }));
|
|
227
|
+
ui.outro(
|
|
228
|
+
forced
|
|
229
|
+
? kleur.red(t('releaseVersion.summaryForced', { version }))
|
|
230
|
+
: kleur.cyan(t('releaseVersion.summaryOptional', { version }))
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = { runReleaseVersion };
|
package/lib/commands/update.js
CHANGED
|
@@ -116,6 +116,29 @@ function applyCoreFiles(projectDir) {
|
|
|
116
116
|
return applyFileList(CORE_FILES, projectDir);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* After an update overwrites files, point the developer to the git-based merge.
|
|
121
|
+
* Deliberately short (3 lines) to keep the CLI clean — the full how-to and the AI
|
|
122
|
+
* merge prompt live in the project's AGENTS.md. Best-effort: stays silent if git
|
|
123
|
+
* is unavailable or nothing actually changed (so a clean update prints nothing).
|
|
124
|
+
*/
|
|
125
|
+
async function printMergeHint(projectDir, t) {
|
|
126
|
+
let changed = [];
|
|
127
|
+
try {
|
|
128
|
+
const { stdout } = await execAsync('git diff --name-only', { cwd: projectDir, env: augmentedEnv() });
|
|
129
|
+
changed = stdout.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
130
|
+
} catch {
|
|
131
|
+
return; // not a git repo, or git not installed — say nothing
|
|
132
|
+
}
|
|
133
|
+
if (changed.length === 0) return;
|
|
134
|
+
ui.note(
|
|
135
|
+
`${t('update.mergeHint.body', { count: changed.length })}\n` +
|
|
136
|
+
`${kleur.dim(t('update.mergeHint.aiPrompt'))}\n` +
|
|
137
|
+
`${kleur.dim(t('update.mergeHint.seeAgents'))}`,
|
|
138
|
+
t('update.mergeHint.title'),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
/** Same detection logic used by add.js and remove.js. */
|
|
120
143
|
async function getActiveModules(kitSetup, projectDir) {
|
|
121
144
|
const modules = [];
|
|
@@ -214,6 +237,7 @@ async function runUpdate(module, options = {}) {
|
|
|
214
237
|
|
|
215
238
|
kitSetup.cliVersion = currentVersion;
|
|
216
239
|
await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
|
|
240
|
+
await printMergeHint(projectDir, t);
|
|
217
241
|
ui.outro(t('update.componentsSuccess'));
|
|
218
242
|
return;
|
|
219
243
|
}
|
|
@@ -262,6 +286,7 @@ async function runUpdate(module, options = {}) {
|
|
|
262
286
|
|
|
263
287
|
kitSetup.cliVersion = currentVersion;
|
|
264
288
|
await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
|
|
289
|
+
await printMergeHint(projectDir, t);
|
|
265
290
|
ui.outro(t('update.coreSuccess'));
|
|
266
291
|
return;
|
|
267
292
|
}
|
|
@@ -301,6 +326,7 @@ async function runUpdate(module, options = {}) {
|
|
|
301
326
|
}
|
|
302
327
|
kitSetup.cliVersion = currentVersion;
|
|
303
328
|
await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
|
|
329
|
+
await printMergeHint(projectDir, t);
|
|
304
330
|
ui.outro(t('update.iosRelease.success'));
|
|
305
331
|
return;
|
|
306
332
|
}
|
|
@@ -392,6 +418,7 @@ async function runUpdate(module, options = {}) {
|
|
|
392
418
|
kitSetup.cliVersion = currentVersion;
|
|
393
419
|
await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
|
|
394
420
|
|
|
421
|
+
await printMergeHint(projectDir, t);
|
|
395
422
|
ui.outro(t('update.success', { module: normalized }));
|
|
396
423
|
return;
|
|
397
424
|
}
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.36.0": {
|
|
3
|
+
"modules": {
|
|
4
|
+
"components": {
|
|
5
|
+
"pt": "Sidebar melhorada: nova linha de busca opcional acima do rodapé (showSearch), variante de gaveta (isDrawer) e modo de demonstração (showcase) pra mais flexibilidade de navegação. As abas internas agora usam o componente compartilhado KasyTabs, deixando o visual mais consistente e o código mais enxuto.",
|
|
6
|
+
"en": "Improved sidebar: new optional search row above the footer (showSearch), a drawer variant (isDrawer) and a showcase mode for more navigation flexibility. The internal tabs now use the shared KasyTabs component, making the look more consistent and the code leaner.",
|
|
7
|
+
"es": "Sidebar mejorada: nueva fila de búsqueda opcional sobre el pie (showSearch), variante de cajón (isDrawer) y modo de demostración (showcase) para más flexibilidad de navegación. Las pestañas internas ahora usan el componente compartido KasyTabs, dejando el aspecto más consistente y el código más limpio."
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"1.35.0": {
|
|
12
|
+
"modules": {
|
|
13
|
+
"core": {
|
|
14
|
+
"pt": "Confiança pra escalar: (1) trava anti-patch-desatualizado no publish — se um arquivo do kit muda mas o patch Supabase/API não acompanha, o `npm publish` falha apontando o que revisar (nunca mais release com backend regredido em silêncio). (2) Atualizar projeto existente ficou claro: depois de `kasy update core/components/<feature>`, a CLI mostra quais arquivos mudaram e como juntar suas customizações com o novo (inclusive um prompt pronto pra IA); o passo a passo também fica no AGENTS.md do projeto. (3) Login Google na web (Supabase): a CLI agora autoriza o localhost no Firebase depois do deploy e com novas tentativas, então o popup do Google na web para de falhar com \"unauthorized-domain\" em projetos recém-criados. (4) Login com Google na web (Supabase): corrigido o erro \"missing sub claim\" que impedia o login Google no navegador. Na web não existe usuário anônimo, então o app agora cria a conta direto em vez de tentar vincular a um anônimo inexistente (mesmo comportamento defensivo do Firebase). (5) Proporção da web refinada: a escala 0.95 vale só no desktop (tablet, mobile e nativo ficam no tamanho real, igual ao device preview), e a compensação de escala alta do sistema agora olha o tamanho da TELA, não da janela, então só estreitar o navegador não encolhe mais a interface.",
|
|
15
|
+
"en": "Confidence to scale: (1) stale-patch tripwire at publish — if a kit file changes but the Supabase/API patch doesn't follow, `npm publish` fails pointing to what to review (no more silently regressed backends in a release). (2) Updating an existing project is now clear: after `kasy update core/components/<feature>`, the CLI shows which files changed and how to merge your customizations with the new version (including a ready AI prompt); the step-by-step also lives in the project AGENTS.md. (3) Google web login (Supabase): the CLI now authorizes localhost on Firebase after the deploy and with retries, so the web Google popup stops failing with \"unauthorized-domain\" on freshly created projects. (4) Google web login (Supabase): fixed the \"missing sub claim\" error that blocked Google login in the browser. On web there is no anonymous user, so the app now creates the account directly instead of trying to link to a non-existent anonymous one (matching the Firebase backend's defensive behavior). (5) Web proportion refined: the 0.95 scale applies on desktop only (tablet, mobile and native render at real size, matching the device preview), and the high-OS-scale compensation now keys off the SCREEN size, not the window, so merely narrowing the browser no longer shrinks the UI.",
|
|
16
|
+
"es": "Confianza para escalar: (1) tripwire de patch desactualizado en el publish — si un archivo del kit cambia pero el patch Supabase/API no lo sigue, `npm publish` falla indicando qué revisar (nunca más un backend regresado en silencio). (2) Actualizar un proyecto existente quedó claro: tras `kasy update core/components/<feature>`, la CLI muestra qué archivos cambiaron y cómo combinar tus personalizaciones con lo nuevo (incluido un prompt listo para IA); el paso a paso también está en el AGENTS.md del proyecto. (3) Inicio de sesión Google en web (Supabase): la CLI ahora autoriza localhost en Firebase después del deploy y con reintentos, así el popup de Google en web deja de fallar con \"unauthorized-domain\" en proyectos recién creados. (4) Inicio de sesión Google en web (Supabase): corregido el error \"missing sub claim\" que bloqueaba el login con Google en el navegador. En web no hay usuario anónimo, así que la app ahora crea la cuenta directamente en vez de intentar vincular a un anónimo inexistente (igual que el comportamiento defensivo del backend Firebase). (5) Proporción web refinada: la escala 0.95 aplica solo en desktop (tablet, móvil y nativo se ven a tamaño real, igual que el device preview), y la compensación de escala alta del sistema ahora usa el tamaño de la PANTALLA, no de la ventana, así que solo estrechar el navegador ya no encoge la interfaz."
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
2
20
|
"1.34.0": {
|
|
3
21
|
"modules": {
|
|
4
22
|
"core": {
|
package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart
CHANGED
|
@@ -222,6 +222,30 @@ class HttpAuthenticationApi implements AuthenticationApi {
|
|
|
222
222
|
@override
|
|
223
223
|
Future<String?> getCurrentUserDisplayName() async => null;
|
|
224
224
|
|
|
225
|
+
@override
|
|
226
|
+
Future<String?> getCurrentUserPhotoUrl() async => null;
|
|
227
|
+
|
|
228
|
+
@override
|
|
229
|
+
Future<List<String>> getLinkedProviders() async => const [];
|
|
230
|
+
|
|
231
|
+
@override
|
|
232
|
+
Future<void> setPassword(String password) {
|
|
233
|
+
// Wire this to your backend, e.g. POST /auth/set-password { password }.
|
|
234
|
+
throw UnimplementedError(
|
|
235
|
+
'❌ Implement setPassword() to send the new password to your backend.',
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@override
|
|
240
|
+
Future<List<String>> linkableSocialProviders() async => const [];
|
|
241
|
+
|
|
242
|
+
@override
|
|
243
|
+
Future<void> linkSocialProvider(String provider) {
|
|
244
|
+
throw UnimplementedError(
|
|
245
|
+
'❌ Implement linkSocialProvider() to link a social provider on your backend.',
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
225
249
|
/// Links an anonymous/guest session to a permanent Google account.
|
|
226
250
|
///
|
|
227
251
|
/// Expected flow:
|
|
@@ -73,6 +73,21 @@ abstract class AuthenticationApi implements OnStartService {
|
|
|
73
73
|
|
|
74
74
|
/// Returns the display name of the current user, or null.
|
|
75
75
|
Future<String?> getCurrentUserDisplayName();
|
|
76
|
+
|
|
77
|
+
/// Returns the photo URL of the current user, or null.
|
|
78
|
+
Future<String?> getCurrentUserPhotoUrl();
|
|
79
|
+
|
|
80
|
+
/// Returns all sign-in providers linked to the current account.
|
|
81
|
+
Future<List<String>> getLinkedProviders();
|
|
82
|
+
|
|
83
|
+
/// Sets or updates an email/password credential for the current user.
|
|
84
|
+
Future<void> setPassword(String password);
|
|
85
|
+
|
|
86
|
+
/// Social providers the current user can still link to their account.
|
|
87
|
+
Future<List<String>> linkableSocialProviders();
|
|
88
|
+
|
|
89
|
+
/// Links a social provider to the current account.
|
|
90
|
+
Future<void> linkSocialProvider(String provider);
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
class PhoneAlreadyLinkedException implements Exception {
|
|
@@ -99,6 +99,18 @@ abstract class AuthenticationRepository {
|
|
|
99
99
|
|
|
100
100
|
/// Returns the display name of the current user, or null.
|
|
101
101
|
Future<String?> getCurrentUserDisplayName();
|
|
102
|
+
|
|
103
|
+
/// Returns the photo URL of the current user, or null.
|
|
104
|
+
Future<String?> getCurrentUserPhotoUrl();
|
|
105
|
+
|
|
106
|
+
/// Returns the sign-in provider used ('google'|'apple'|'facebook'|'email'|'phone'), or null.
|
|
107
|
+
Future<List<String>> getLinkedProviders();
|
|
108
|
+
|
|
109
|
+
Future<void> setPassword(String password);
|
|
110
|
+
|
|
111
|
+
Future<List<String>> linkableSocialProviders();
|
|
112
|
+
|
|
113
|
+
Future<void> linkSocialProvider(String provider);
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
/// this is an example on how to create an authentication repository using firebase
|
|
@@ -403,4 +415,19 @@ class HttpAuthenticationRepository implements AuthenticationRepository {
|
|
|
403
415
|
|
|
404
416
|
@override
|
|
405
417
|
Future<String?> getCurrentUserDisplayName() => _authenticationApi.getCurrentUserDisplayName();
|
|
418
|
+
|
|
419
|
+
@override
|
|
420
|
+
Future<String?> getCurrentUserPhotoUrl() => _authenticationApi.getCurrentUserPhotoUrl();
|
|
421
|
+
|
|
422
|
+
@override
|
|
423
|
+
Future<List<String>> getLinkedProviders() => _authenticationApi.getLinkedProviders();
|
|
424
|
+
|
|
425
|
+
@override
|
|
426
|
+
Future<void> setPassword(String password) => _authenticationApi.setPassword(password);
|
|
427
|
+
|
|
428
|
+
@override
|
|
429
|
+
Future<List<String>> linkableSocialProviders() => _authenticationApi.linkableSocialProviders();
|
|
430
|
+
|
|
431
|
+
@override
|
|
432
|
+
Future<void> linkSocialProvider(String provider) => _authenticationApi.linkSocialProvider(provider);
|
|
406
433
|
}
|
|
@@ -36,6 +36,7 @@ sealed class SubscriptionEntity with _$SubscriptionEntity {
|
|
|
36
36
|
@JsonKey(name: 'creation_date') DateTime? creationDate,
|
|
37
37
|
@JsonKey(name: 'last_update_date') DateTime? lastUpdateDate,
|
|
38
38
|
@JsonKey(name: 'period_end_date') DateTime? periodEndDate,
|
|
39
|
+
@JsonKey(name: 'trial_end') DateTime? trialEnd,
|
|
39
40
|
@JsonKey(name: 'status') required SubscriptionStatus status,
|
|
40
41
|
@JsonKey(name: 'store', unknownEnumValue: SubscriptionStore.unknown)
|
|
41
42
|
SubscriptionStore? store,
|
|
@@ -710,37 +710,51 @@ async function checkBillingEnabled(projectId) {
|
|
|
710
710
|
* with only [localhost] would wipe firebaseapp.com / web.app. Idempotent: it's a
|
|
711
711
|
* no-op when both entries are already present.
|
|
712
712
|
*
|
|
713
|
+
* Right after a project's auth config is initialized, the Admin v2 config endpoint
|
|
714
|
+
* can briefly answer 403/404/409 before it settles (and a PATCH can 409 on a
|
|
715
|
+
* concurrent edit). Those are retried — re-reading each round so the merge stays
|
|
716
|
+
* correct; 400 and other client errors are treated as fatal. Best-effort: returns
|
|
717
|
+
* { ok: false } after exhausting retries instead of throwing.
|
|
718
|
+
*
|
|
719
|
+
* @param {string} projectId
|
|
720
|
+
* @param {string} token
|
|
721
|
+
* @param {{ maxRetries?: number, retryDelayMs?: number }} [opts]
|
|
713
722
|
* @returns {{ ok: boolean, added?: string[], error?: string }}
|
|
714
723
|
*/
|
|
715
|
-
async function ensureLocalhostAuthorizedDomains(projectId, token) {
|
|
724
|
+
async function ensureLocalhostAuthorizedDomains(projectId, token, { maxRetries = 4, retryDelayMs = 4000 } = {}) {
|
|
716
725
|
const base = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/config`;
|
|
717
726
|
const headers = {
|
|
718
727
|
Authorization: `Bearer ${token}`,
|
|
719
728
|
'Content-Type': 'application/json',
|
|
720
729
|
'X-Goog-User-Project': projectId,
|
|
721
730
|
};
|
|
722
|
-
// 1. Read the current authorized domains.
|
|
723
|
-
const getRes = await fetch(base, { headers });
|
|
724
|
-
if (!getRes.ok) {
|
|
725
|
-
const text = await getRes.text();
|
|
726
|
-
return { ok: false, error: `${getRes.status}: ${text}` };
|
|
727
|
-
}
|
|
728
|
-
const config = await getRes.json();
|
|
729
|
-
const current = Array.isArray(config.authorizedDomains) ? config.authorizedDomains : [];
|
|
730
731
|
const required = ['localhost', '127.0.0.1'];
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
732
|
+
const TRANSIENT = new Set([403, 404, 409, 429, 500, 503]);
|
|
733
|
+
let lastError = 'unknown';
|
|
734
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
735
|
+
// 1. Read the current authorized domains.
|
|
736
|
+
const getRes = await fetch(base, { headers });
|
|
737
|
+
if (!getRes.ok) {
|
|
738
|
+
lastError = `GET ${getRes.status}: ${await getRes.text()}`;
|
|
739
|
+
if (attempt < maxRetries && TRANSIENT.has(getRes.status)) { await sleep(retryDelayMs); continue; }
|
|
740
|
+
return { ok: false, error: lastError };
|
|
741
|
+
}
|
|
742
|
+
const config = await getRes.json();
|
|
743
|
+
const current = Array.isArray(config.authorizedDomains) ? config.authorizedDomains : [];
|
|
744
|
+
const missing = required.filter((d) => !current.includes(d));
|
|
745
|
+
if (missing.length === 0) return { ok: true, added: [] };
|
|
746
|
+
// 2. Merge and write back, keeping every domain that was already there.
|
|
747
|
+
const patchRes = await fetch(`${base}?updateMask=authorizedDomains`, {
|
|
748
|
+
method: 'PATCH',
|
|
749
|
+
headers,
|
|
750
|
+
body: JSON.stringify({ authorizedDomains: [...current, ...missing] }),
|
|
751
|
+
});
|
|
752
|
+
if (patchRes.ok) return { ok: true, added: missing };
|
|
753
|
+
lastError = `PATCH ${patchRes.status}: ${await patchRes.text()}`;
|
|
754
|
+
if (attempt < maxRetries && TRANSIENT.has(patchRes.status)) { await sleep(retryDelayMs); continue; }
|
|
755
|
+
return { ok: false, error: lastError };
|
|
742
756
|
}
|
|
743
|
-
return { ok:
|
|
757
|
+
return { ok: false, error: lastError };
|
|
744
758
|
}
|
|
745
759
|
|
|
746
760
|
/**
|