kasy-cli 1.32.0 → 1.35.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 +66 -2
- package/docs/cli-reference.md +7 -7
- package/lib/commands/apple-web.js +222 -0
- package/lib/commands/configure.js +3 -91
- package/lib/commands/doctor.js +20 -0
- package/lib/commands/facebook.js +189 -0
- package/lib/commands/new.js +61 -11
- package/lib/commands/release-version.js +234 -0
- package/lib/commands/update.js +27 -0
- package/lib/scaffold/CHANGELOG.json +27 -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 +199 -21
- package/lib/scaffold/backends/patch-base-hashes.json +66 -0
- package/lib/scaffold/backends/supabase/deploy.js +92 -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 +92 -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/scaffold/shared/generator-utils.js +18 -6
- package/lib/utils/apple-web.js +147 -0
- package/lib/utils/facebook.js +162 -0
- package/lib/utils/i18n/messages-en.js +85 -0
- package/lib/utils/i18n/messages-es.js +85 -0
- package/lib/utils/i18n/messages-pt.js +85 -0
- package/package.json +5 -2
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
- package/templates/firebase/AGENTS.md +170 -0
- package/templates/firebase/CLAUDE.md +16 -0
- package/templates/firebase/DESIGN_SYSTEM.md +269 -0
- package/templates/firebase/docs/auth-setup.en.md +4 -2
- package/templates/firebase/docs/auth-setup.es.md +4 -2
- package/templates/firebase/docs/auth-setup.pt.md +4 -2
- 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/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_alert.dart +0 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +35 -17
- 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_screen.dart +114 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
- 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 +108 -73
- 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/chrome/chrome_visibility.dart +22 -0
- package/templates/firebase/lib/core/config/features.dart +5 -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 +48 -124
- 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/core/widgets/update_bottom_sheet.dart +29 -126
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -8
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
- 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 +266 -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 +80 -15
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +20 -14
- 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/feedbacks/ui/widgets/feature_card.dart +1 -2
- package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
- package/templates/firebase/lib/features/home/home_components_page.dart +8 -1
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +186 -56
- package/templates/firebase/lib/features/home/home_page.dart +4 -0
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +169 -208
- package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
- package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
- package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -4
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +84 -128
- 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/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +2 -1
- package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +58 -21
- 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/avatar_component.dart +2 -2
- 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 +54 -7
- package/templates/firebase/lib/i18n/es.i18n.json +54 -7
- package/templates/firebase/lib/i18n/pt.i18n.json +54 -7
- package/templates/firebase/lib/main.dart +11 -2
- package/templates/firebase/lib/router.dart +94 -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/tool/design_check.dart +152 -0
- package/templates/firebase/web/index.html +162 -14
- package/templates/firebase/assets/images/review.png +0 -0
- package/templates/firebase/assets/images/update.png +0 -0
- package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
- package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
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,7 +8,10 @@ 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');
|
|
13
|
+
const { runAppleWeb } = require('../lib/commands/apple-web');
|
|
14
|
+
const { runFacebook } = require('../lib/commands/facebook');
|
|
12
15
|
const { runCheck } = require('../lib/commands/check');
|
|
13
16
|
const { runRun } = require('../lib/commands/run');
|
|
14
17
|
const { runReset } = require('../lib/commands/reset');
|
|
@@ -325,6 +328,27 @@ function buildProgram(language) {
|
|
|
325
328
|
t
|
|
326
329
|
);
|
|
327
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
|
+
|
|
328
352
|
applyLocalizedHelp(
|
|
329
353
|
program
|
|
330
354
|
.command('configure')
|
|
@@ -336,6 +360,46 @@ function buildProgram(language) {
|
|
|
336
360
|
t
|
|
337
361
|
);
|
|
338
362
|
|
|
363
|
+
applyLocalizedHelp(
|
|
364
|
+
program
|
|
365
|
+
.command('apple-web')
|
|
366
|
+
.argument('[directory]', 'Project folder (default: current directory)', '.')
|
|
367
|
+
.option('--service-id <id>', 'Apple Service ID (the OAuth client_id), e.g. com.acme.app.signin')
|
|
368
|
+
.option('--team-id <id>', 'Apple Developer Team ID')
|
|
369
|
+
.option('--key-id <id>', 'Key ID of the .p8')
|
|
370
|
+
.option('--p8 <path>', 'Path to the Sign In with Apple .p8 private key')
|
|
371
|
+
.description(t('cli.command.appleWeb.description'))
|
|
372
|
+
.action(async (directory, options) => {
|
|
373
|
+
await runAppleWeb(directory, {
|
|
374
|
+
language,
|
|
375
|
+
serviceId: options.serviceId,
|
|
376
|
+
teamId: options.teamId,
|
|
377
|
+
keyId: options.keyId,
|
|
378
|
+
p8: options.p8,
|
|
379
|
+
});
|
|
380
|
+
}),
|
|
381
|
+
t
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
applyLocalizedHelp(
|
|
385
|
+
program
|
|
386
|
+
.command('facebook')
|
|
387
|
+
.argument('[directory]', 'Project folder (default: current directory)', '.')
|
|
388
|
+
.option('--app-id <id>', 'Meta App ID')
|
|
389
|
+
.option('--client-token <token>', 'Meta Client Token (Settings → Advanced)')
|
|
390
|
+
.option('--app-secret <secret>', 'Meta App Secret (for the Firebase/Supabase provider)')
|
|
391
|
+
.description(t('cli.command.facebook.description'))
|
|
392
|
+
.action(async (directory, options) => {
|
|
393
|
+
await runFacebook(directory, {
|
|
394
|
+
language,
|
|
395
|
+
appId: options.appId,
|
|
396
|
+
clientToken: options.clientToken,
|
|
397
|
+
appSecret: options.appSecret,
|
|
398
|
+
});
|
|
399
|
+
}),
|
|
400
|
+
t
|
|
401
|
+
);
|
|
402
|
+
|
|
339
403
|
applyLocalizedHelp(
|
|
340
404
|
program
|
|
341
405
|
.command('check')
|
|
@@ -390,7 +454,7 @@ function buildProgram(language) {
|
|
|
390
454
|
applyLocalizedHelp(
|
|
391
455
|
program
|
|
392
456
|
.command('add')
|
|
393
|
-
.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...)')
|
|
394
458
|
.option('--list', 'List all available features and their current status')
|
|
395
459
|
.option('--yes', 'Skip interactive prompts (use placeholder values)')
|
|
396
460
|
.option('-d, --directory <path>', 'Project folder (default: current directory)', '.')
|
|
@@ -404,7 +468,7 @@ function buildProgram(language) {
|
|
|
404
468
|
applyLocalizedHelp(
|
|
405
469
|
program
|
|
406
470
|
.command('remove')
|
|
407
|
-
.argument('[feature]', 'Feature to remove (e.g. sentry, analytics,
|
|
471
|
+
.argument('[feature]', 'Feature to remove (e.g. sentry, analytics, ai_chat, ci...)')
|
|
408
472
|
.option('--yes', 'Skip confirmation prompt')
|
|
409
473
|
.option('-d, --directory <path>', 'Project folder (default: current directory)', '.')
|
|
410
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 |
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kasy apple-web` — configure "Sign in with Apple" on the WEB.
|
|
3
|
+
*
|
|
4
|
+
* Apple web in-app is Firebase-only for now. Native iOS/macOS is already enabled by
|
|
5
|
+
* `kasy new` on every backend. The web flow needs a Service ID + a `.p8`, created
|
|
6
|
+
* once on developer.apple.com (no API for that — stays manual). This command:
|
|
7
|
+
* - Firebase: writes the Apple provider's codeFlowConfig (Service ID + Team ID +
|
|
8
|
+
* Key ID + `.p8`) and flips withAppleWebSignin true. Firebase re-signs the secret
|
|
9
|
+
* itself, so it never expires.
|
|
10
|
+
* - Supabase / API: web Apple isn't wired in the app yet (roadmap, see ROADMAP.md).
|
|
11
|
+
* The command only prints a note and configures nothing / flips no flag.
|
|
12
|
+
*
|
|
13
|
+
* The inputs are cached in ~/.kasy/apple-web.json so future Firebase projects
|
|
14
|
+
* configure web Apple without asking again.
|
|
15
|
+
*
|
|
16
|
+
* Note: enableAppleWebSignIn() in supabase/deploy.js is a tested roadmap helper for
|
|
17
|
+
* the future Supabase web flow; it is not invoked by this command yet.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
const os = require('node:os');
|
|
22
|
+
const fs = require('fs-extra');
|
|
23
|
+
const kleur = require('kleur');
|
|
24
|
+
const ui = require('../utils/ui');
|
|
25
|
+
const { printCompactHeader } = require('../utils/brand');
|
|
26
|
+
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
27
|
+
const { loadAppleWebCreds, saveAppleWebCreds } = require('../utils/apple-web');
|
|
28
|
+
const { configureFirebaseAppleWeb } = require('../scaffold/backends/firebase/setup-from-scratch');
|
|
29
|
+
|
|
30
|
+
/** Expand a leading ~ to the user's home directory. */
|
|
31
|
+
function expandHome(p) {
|
|
32
|
+
if (!p) return p;
|
|
33
|
+
return p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Set `withAppleWebSignin` to the given boolean in the project's features.dart. */
|
|
37
|
+
async function setAppleWebFlag(projectDir, value) {
|
|
38
|
+
const file = path.join(projectDir, 'lib', 'core', 'config', 'features.dart');
|
|
39
|
+
if (!(await fs.pathExists(file))) return { ok: false, missing: true };
|
|
40
|
+
let content = await fs.readFile(file, 'utf8');
|
|
41
|
+
const re = /const bool withAppleWebSignin\s*=\s*(?:true|false)\s*;/;
|
|
42
|
+
if (!re.test(content)) return { ok: false, missing: true };
|
|
43
|
+
content = content.replace(re, `const bool withAppleWebSignin = ${value};`);
|
|
44
|
+
await fs.writeFile(file, content, 'utf8');
|
|
45
|
+
return { ok: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the four Apple inputs: from CLI flags, then the cache, then prompts.
|
|
50
|
+
* Reads the `.p8` from a file path when given. Returns null on any read failure.
|
|
51
|
+
*/
|
|
52
|
+
async function resolveCreds(options, t) {
|
|
53
|
+
const cached = await loadAppleWebCreds();
|
|
54
|
+
|
|
55
|
+
// Non-interactive / flag-driven path.
|
|
56
|
+
if (options.serviceId || options.teamId || options.keyId || options.p8) {
|
|
57
|
+
const privateKey = options.p8
|
|
58
|
+
? await fs.readFile(expandHome(options.p8), 'utf8').catch(() => null)
|
|
59
|
+
: cached?.privateKey;
|
|
60
|
+
if (options.p8 && !privateKey) {
|
|
61
|
+
ui.log.error(t('appleWeb.p8ReadError'));
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
serviceId: options.serviceId || cached?.serviceId,
|
|
66
|
+
teamId: options.teamId || cached?.teamId,
|
|
67
|
+
keyId: options.keyId || cached?.keyId,
|
|
68
|
+
privateKey,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Reuse the cache without asking, unless the user wants to replace it.
|
|
73
|
+
if (cached) {
|
|
74
|
+
const reuse = await ui.confirm({
|
|
75
|
+
message: t('appleWeb.useSaved').replace('{id}', cached.serviceId),
|
|
76
|
+
initialValue: true,
|
|
77
|
+
});
|
|
78
|
+
if (reuse) return cached;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Interactive prompts.
|
|
82
|
+
const serviceId = await ui.text({
|
|
83
|
+
message: t('appleWeb.serviceIdPrompt'),
|
|
84
|
+
placeholder: 'com.acme.app.signin',
|
|
85
|
+
validate: (v) => (v && v.trim() ? undefined : t('appleWeb.required')),
|
|
86
|
+
});
|
|
87
|
+
const teamId = await ui.text({
|
|
88
|
+
message: t('appleWeb.teamIdPrompt'),
|
|
89
|
+
placeholder: 'ABCDE12345',
|
|
90
|
+
validate: (v) => (v && v.trim() ? undefined : t('appleWeb.required')),
|
|
91
|
+
});
|
|
92
|
+
const keyId = await ui.text({
|
|
93
|
+
message: t('appleWeb.keyIdPrompt'),
|
|
94
|
+
placeholder: '6RR89XG535',
|
|
95
|
+
validate: (v) => (v && v.trim() ? undefined : t('appleWeb.required')),
|
|
96
|
+
});
|
|
97
|
+
const p8Path = await ui.text({
|
|
98
|
+
message: t('appleWeb.p8Prompt'),
|
|
99
|
+
placeholder: '~/Downloads/AuthKey_6RR89XG535.p8',
|
|
100
|
+
validate: (v) => (v && v.trim() ? undefined : t('appleWeb.required')),
|
|
101
|
+
});
|
|
102
|
+
const privateKey = await fs.readFile(expandHome(p8Path.trim()), 'utf8').catch(() => null);
|
|
103
|
+
if (!privateKey) {
|
|
104
|
+
ui.log.error(t('appleWeb.p8ReadError'));
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
serviceId: serviceId.trim(),
|
|
109
|
+
teamId: teamId.trim(),
|
|
110
|
+
keyId: keyId.trim(),
|
|
111
|
+
privateKey,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function runAppleWeb(directory, options = {}) {
|
|
116
|
+
const language = options.language || detectDefaultLanguage();
|
|
117
|
+
const t = createTranslator(language);
|
|
118
|
+
printCompactHeader(t);
|
|
119
|
+
ui.intro(kleur.bold(t('appleWeb.title')));
|
|
120
|
+
|
|
121
|
+
const projectDir = path.resolve(directory || '.');
|
|
122
|
+
const kitSetupPath = path.join(projectDir, 'kit_setup.json');
|
|
123
|
+
if (!(await fs.pathExists(kitSetupPath))) {
|
|
124
|
+
ui.cancel(t('appleWeb.notKasyProject'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let kit;
|
|
129
|
+
try {
|
|
130
|
+
kit = await fs.readJson(kitSetupPath);
|
|
131
|
+
} catch {
|
|
132
|
+
ui.cancel(t('appleWeb.notKasyProject'));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const backend = kit.backendProvider || 'firebase';
|
|
137
|
+
const bundleId = kit.bundleId || null;
|
|
138
|
+
|
|
139
|
+
// Apple web in-app works on the Firebase backend only (signInWithPopup). On
|
|
140
|
+
// Supabase the web flow isn't wired in the app yet (roadmap) and the API backend
|
|
141
|
+
// owns its own auth — in both cases there's nothing to configure here for web Apple
|
|
142
|
+
// (native iOS is already set up by `kasy new`).
|
|
143
|
+
if (backend !== 'firebase') {
|
|
144
|
+
ui.note(t('appleWeb.notFirebase'), t('appleWeb.manualTitle'));
|
|
145
|
+
ui.outro(t('appleWeb.notFirebaseDone'));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const projectId = kit.firebaseProjectId;
|
|
150
|
+
if (!projectId) {
|
|
151
|
+
ui.cancel(t('appleWeb.missingProjectId'));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const creds = await resolveCreds(options, t);
|
|
156
|
+
if (!creds) return;
|
|
157
|
+
if (!creds.serviceId || !creds.teamId || !creds.keyId || !creds.privateKey) {
|
|
158
|
+
ui.cancel(t('appleWeb.incomplete'));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Cache for future projects.
|
|
163
|
+
await saveAppleWebCreds(creds);
|
|
164
|
+
|
|
165
|
+
const s = ui.spinner();
|
|
166
|
+
s.start(t('appleWeb.configuringFirebase'));
|
|
167
|
+
const result = await configureFirebaseAppleWeb({ projectId, bundleId, ...creds });
|
|
168
|
+
if (!result.ok) {
|
|
169
|
+
s.stop(t('appleWeb.failed'));
|
|
170
|
+
ui.log.error(result.error || 'unknown error');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
s.stop(t('appleWeb.firebaseOk'));
|
|
174
|
+
|
|
175
|
+
// Show the Apple button on web now that it actually works.
|
|
176
|
+
const flag = await setAppleWebFlag(projectDir, true);
|
|
177
|
+
if (flag.ok) ui.log.success(t('appleWeb.flagUpdated'));
|
|
178
|
+
else ui.log.warn(t('appleWeb.flagMissing'));
|
|
179
|
+
|
|
180
|
+
// Remind the developer of the manual Return URL their Service ID must carry.
|
|
181
|
+
const returnUrl = `https://${projectId}.firebaseapp.com/__/auth/handler`;
|
|
182
|
+
ui.note(`${t('appleWeb.returnUrlNote')}\n${kleur.cyan(returnUrl)}`, t('appleWeb.manualTitle'));
|
|
183
|
+
ui.outro(t('appleWeb.allDone'));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Called by `kasy new`: if the developer already saved Apple web credentials on a
|
|
188
|
+
* previous project (`kasy apple-web`), configure web Apple for the freshly created
|
|
189
|
+
* project automatically — same behavior in both quick and advanced modes. No
|
|
190
|
+
* prompts: it either applies (creds cached) or reports pending.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} targetDir - the generated project directory
|
|
193
|
+
* @returns {{ applied: boolean, pending: boolean, backend?: string, error?: string }}
|
|
194
|
+
*/
|
|
195
|
+
async function autoApplyAppleWebIfCached(targetDir) {
|
|
196
|
+
let kit;
|
|
197
|
+
try {
|
|
198
|
+
kit = await fs.readJson(path.join(targetDir, 'kit_setup.json'));
|
|
199
|
+
} catch {
|
|
200
|
+
return { applied: false, pending: false };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const backend = kit.backendProvider || 'firebase';
|
|
204
|
+
// Apple web in-app is Firebase-only for now (Supabase web = roadmap, API = own
|
|
205
|
+
// server). On those backends there's nothing to apply and nothing pending.
|
|
206
|
+
if (backend !== 'firebase') return { applied: false, pending: false, backend };
|
|
207
|
+
|
|
208
|
+
const projectId = kit.firebaseProjectId;
|
|
209
|
+
if (!projectId) return { applied: false, pending: false, backend };
|
|
210
|
+
|
|
211
|
+
const creds = await loadAppleWebCreds();
|
|
212
|
+
if (!creds) return { applied: false, pending: true, backend };
|
|
213
|
+
|
|
214
|
+
const bundleId = kit.bundleId || null;
|
|
215
|
+
const result = await configureFirebaseAppleWeb({ projectId, bundleId, ...creds });
|
|
216
|
+
if (!result.ok) return { applied: false, pending: true, backend, error: result.error };
|
|
217
|
+
|
|
218
|
+
await setAppleWebFlag(targetDir, true);
|
|
219
|
+
return { applied: true, pending: false, backend };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = { runAppleWeb, setAppleWebFlag, autoApplyAppleWebIfCached };
|
|
@@ -20,6 +20,7 @@ const kleur = require('kleur');
|
|
|
20
20
|
const ui = require('../utils/ui');
|
|
21
21
|
const { printCompactHeader } = require('../utils/brand');
|
|
22
22
|
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
23
|
+
const { readFacebookState, writeFacebookCredentials } = require('../utils/facebook');
|
|
23
24
|
|
|
24
25
|
const PLACEHOLDER_REGEX = /^(YOUR_|TODO|CHANGE_ME|<.*>$)/i;
|
|
25
26
|
|
|
@@ -261,97 +262,8 @@ async function updateEnvFile(envPath, updates) {
|
|
|
261
262
|
await fs.outputFile(envPath, updated.join('\n'), 'utf8');
|
|
262
263
|
}
|
|
263
264
|
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
// Android strings.xml. Both ship with placeholder zeros; we replace them in
|
|
267
|
-
// place so the user only types each value once.
|
|
268
|
-
|
|
269
|
-
function readFacebookCurrent(content, kind) {
|
|
270
|
-
if (kind === 'plist') {
|
|
271
|
-
const appId = content.match(/<key>FacebookAppID<\/key>\s*<string>([^<]*)<\/string>/);
|
|
272
|
-
const token = content.match(/<key>FacebookClientToken<\/key>\s*<string>([^<]*)<\/string>/);
|
|
273
|
-
return { appId: (appId?.[1] || '').trim(), token: (token?.[1] || '').trim() };
|
|
274
|
-
}
|
|
275
|
-
// strings.xml
|
|
276
|
-
const appId = content.match(/<string name="facebook_app_id"[^>]*>([^<]*)<\/string>/);
|
|
277
|
-
const token = content.match(/<string name="facebook_client_token"[^>]*>([^<]*)<\/string>/);
|
|
278
|
-
return { appId: (appId?.[1] || '').trim(), token: (token?.[1] || '').trim() };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function isFacebookPlaceholder(value) {
|
|
282
|
-
return !value || /^0+$/.test(value);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async function readFacebookState(projectDir) {
|
|
286
|
-
const plistPath = path.join(projectDir, 'ios', 'Runner', 'Info.plist');
|
|
287
|
-
const stringsPath = path.join(projectDir, 'android', 'app', 'src', 'main', 'res', 'values', 'strings.xml');
|
|
288
|
-
const state = { appId: '', token: '', plistExists: false, stringsExists: false };
|
|
289
|
-
if (await fs.pathExists(plistPath)) {
|
|
290
|
-
state.plistExists = true;
|
|
291
|
-
const plistRead = readFacebookCurrent(await fs.readFile(plistPath, 'utf8'), 'plist');
|
|
292
|
-
if (!isFacebookPlaceholder(plistRead.appId)) state.appId = plistRead.appId;
|
|
293
|
-
if (!isFacebookPlaceholder(plistRead.token)) state.token = plistRead.token;
|
|
294
|
-
}
|
|
295
|
-
if (await fs.pathExists(stringsPath)) {
|
|
296
|
-
state.stringsExists = true;
|
|
297
|
-
const stringsRead = readFacebookCurrent(await fs.readFile(stringsPath, 'utf8'), 'strings');
|
|
298
|
-
// Prefer plist value when both exist; only fall back if plist was placeholder.
|
|
299
|
-
if (!state.appId && !isFacebookPlaceholder(stringsRead.appId)) state.appId = stringsRead.appId;
|
|
300
|
-
if (!state.token && !isFacebookPlaceholder(stringsRead.token)) state.token = stringsRead.token;
|
|
301
|
-
}
|
|
302
|
-
return state;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async function writeFacebookCredentials(projectDir, appId, clientToken) {
|
|
306
|
-
const plistPath = path.join(projectDir, 'ios', 'Runner', 'Info.plist');
|
|
307
|
-
const stringsPath = path.join(projectDir, 'android', 'app', 'src', 'main', 'res', 'values', 'strings.xml');
|
|
308
|
-
const results = { plist: 'skipped', strings: 'skipped' };
|
|
309
|
-
|
|
310
|
-
if (appId && await fs.pathExists(plistPath)) {
|
|
311
|
-
let plist = await fs.readFile(plistPath, 'utf8');
|
|
312
|
-
plist = plist.replace(
|
|
313
|
-
/(<key>FacebookAppID<\/key>\s*<string>)[^<]*(<\/string>)/,
|
|
314
|
-
`$1${appId}$2`
|
|
315
|
-
);
|
|
316
|
-
if (clientToken) {
|
|
317
|
-
plist = plist.replace(
|
|
318
|
-
/(<key>FacebookClientToken<\/key>\s*<string>)[^<]*(<\/string>)/,
|
|
319
|
-
`$1${clientToken}$2`
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
// CFBundleURLSchemes — replace `fb<zeros>` with `fb<appId>` so deep links work.
|
|
323
|
-
plist = plist.replace(/<string>fb0+<\/string>/, `<string>fb${appId}</string>`);
|
|
324
|
-
await fs.outputFile(plistPath, plist, 'utf8');
|
|
325
|
-
results.plist = 'ok';
|
|
326
|
-
} else if (appId) {
|
|
327
|
-
results.plist = 'missing_file';
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (appId && await fs.pathExists(stringsPath)) {
|
|
331
|
-
let xml = await fs.readFile(stringsPath, 'utf8');
|
|
332
|
-
xml = xml.replace(
|
|
333
|
-
/(<string name="facebook_app_id"[^>]*>)[^<]*(<\/string>)/,
|
|
334
|
-
`$1${appId}$2`
|
|
335
|
-
);
|
|
336
|
-
if (clientToken) {
|
|
337
|
-
xml = xml.replace(
|
|
338
|
-
/(<string name="facebook_client_token"[^>]*>)[^<]*(<\/string>)/,
|
|
339
|
-
`$1${clientToken}$2`
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
// fb_login_protocol_scheme — set if present.
|
|
343
|
-
xml = xml.replace(
|
|
344
|
-
/(<string name="fb_login_protocol_scheme"[^>]*>)[^<]*(<\/string>)/,
|
|
345
|
-
`$1fb${appId}$2`
|
|
346
|
-
);
|
|
347
|
-
await fs.outputFile(stringsPath, xml, 'utf8');
|
|
348
|
-
results.strings = 'ok';
|
|
349
|
-
} else if (appId) {
|
|
350
|
-
results.strings = 'missing_file';
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return results;
|
|
354
|
-
}
|
|
265
|
+
// Facebook native build-time files (Info.plist + strings.xml) read/write moved to
|
|
266
|
+
// lib/utils/facebook.js so `kasy configure` and `kasy facebook` share one source.
|
|
355
267
|
|
|
356
268
|
async function setFirebaseSecret(projectDir, key, value, t) {
|
|
357
269
|
// Use a tmp file to avoid newline injection / shell escaping issues.
|
package/lib/commands/doctor.js
CHANGED
|
@@ -151,6 +151,26 @@ async function runIosReleaseChecks(projectDir, t, language, config = {}) {
|
|
|
151
151
|
ui.log.warn(`${t('doctor.ios.appleSignInEntitlementMissing')}\n${kleur.dim(appleEntitlement.error)}`);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// Apple Sign-In on the WEB (Firebase/Supabase). Native iOS is covered above; the
|
|
155
|
+
// web button only ships once configured, so surface whether it's done or pending.
|
|
156
|
+
if (config.backendProvider === 'firebase' || config.backendProvider === 'supabase') {
|
|
157
|
+
let webConfigured = false;
|
|
158
|
+
try {
|
|
159
|
+
const featuresContent = await fs.readFile(
|
|
160
|
+
path.join(projectDir, 'lib', 'core', 'config', 'features.dart'),
|
|
161
|
+
'utf8',
|
|
162
|
+
);
|
|
163
|
+
webConfigured = /const bool withAppleWebSignin\s*=\s*true\s*;/.test(featuresContent);
|
|
164
|
+
} catch {
|
|
165
|
+
// features.dart not found — leave as not configured.
|
|
166
|
+
}
|
|
167
|
+
if (webConfigured) {
|
|
168
|
+
ui.log.success(t('doctor.appleWeb.ok'));
|
|
169
|
+
} else {
|
|
170
|
+
ui.log.message(kleur.dim(`– ${t('doctor.appleWeb.pending')}`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
154
174
|
if (config.withFacebookPixel) {
|
|
155
175
|
const facebook = await validateFacebookInfoPlist(projectDir);
|
|
156
176
|
if (facebook.skipped) {
|