kasy-cli 1.31.14 → 1.34.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 +42 -0
- 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 +65 -3
- package/lib/scaffold/CHANGELOG.json +27 -0
- package/lib/scaffold/backends/api/patch/README.md +87 -2
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +34 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +186 -0
- package/lib/scaffold/backends/supabase/deploy.js +92 -0
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +26 -22
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -11
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +3 -1
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +60 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +22 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +3 -2
- package/lib/scaffold/generate.js +1 -1
- package/lib/scaffold/shared/generator-utils.js +34 -3
- package/lib/utils/apple-web.js +147 -0
- package/lib/utils/facebook.js +162 -0
- package/lib/utils/i18n/messages-en.js +64 -0
- package/lib/utils/i18n/messages-es.js +64 -0
- package/lib/utils/i18n/messages-pt.js +64 -0
- package/package.json +2 -2
- package/templates/firebase/AGENTS.md +87 -0
- package/templates/firebase/CLAUDE.md +16 -0
- package/templates/firebase/DESIGN_SYSTEM.md +234 -0
- package/templates/firebase/docs/auth-setup.en.md +7 -1
- package/templates/firebase/docs/auth-setup.es.md +7 -1
- package/templates/firebase/docs/auth-setup.pt.md +7 -1
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +79 -5
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_accordion.dart +2 -2
- package/templates/firebase/lib/components/kasy_alert.dart +1 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +7 -4
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +1 -1
- package/templates/firebase/lib/components/kasy_button.dart +8 -8
- package/templates/firebase/lib/components/kasy_chip.dart +1 -1
- package/templates/firebase/lib/components/kasy_date_picker.dart +26 -21
- package/templates/firebase/lib/components/kasy_dialog.dart +2 -2
- package/templates/firebase/lib/components/kasy_screen.dart +114 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +2 -2
- package/templates/firebase/lib/components/kasy_tabs.dart +2 -2
- package/templates/firebase/lib/components/kasy_text_area.dart +37 -5
- package/templates/firebase/lib/components/kasy_text_field.dart +77 -16
- package/templates/firebase/lib/components/kasy_toast.dart +39 -70
- package/templates/firebase/lib/components/kasy_web_header.dart +4 -3
- package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
- package/templates/firebase/lib/core/config/features.dart +18 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +21 -0
- package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +1 -1
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +46 -124
- package/templates/firebase/lib/core/theme/icon_sizes.dart +47 -0
- package/templates/firebase/lib/core/theme/shadows.dart +13 -0
- package/templates/firebase/lib/core/theme/texts.dart +32 -0
- package/templates/firebase/lib/core/theme/theme.dart +2 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -0
- package/templates/firebase/lib/core/web_viewport_scale.dart +23 -4
- package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -7
- 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_conversation_list.dart +1 -1
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -1
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +61 -0
- package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +1 -1
- package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -1
- package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +3 -1
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +57 -29
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +47 -25
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +1 -1
- package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +1 -1
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +1 -1
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +2 -3
- package/templates/firebase/lib/features/home/home_components_page.dart +7 -1
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +54 -3
- package/templates/firebase/lib/features/home/home_image_grid.dart +1 -1
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +165 -209
- 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 -6
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +6 -1
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +104 -156
- package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +1 -1
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +1 -1
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +3 -3
- 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 +3 -2
- package/templates/firebase/lib/features/settings/settings_page.dart +264 -307
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +17 -8
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +4 -4
- package/templates/firebase/lib/features/settings/ui/components/edit_name_sheet.dart +115 -0
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +2 -2
- package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +13 -5
- package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
- package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +7 -1
- package/templates/firebase/lib/features/subscriptions/providers/premium_page_provider.dart +11 -3
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +2 -2
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +3 -3
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +1 -1
- package/templates/firebase/lib/i18n/en.i18n.json +13 -4
- package/templates/firebase/lib/i18n/es.i18n.json +13 -4
- package/templates/firebase/lib/i18n/pt.i18n.json +13 -4
- package/templates/firebase/lib/router.dart +2 -0
- package/templates/firebase/pubspec.yaml +1 -2
- package/templates/firebase/tool/design_check.dart +152 -0
- package/templates/firebase/web/stripe_success.html +64 -26
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
- package/templates/firebase/assets/images/review.png +0 -0
- package/templates/firebase/assets/images/update.png +0 -0
- package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +0 -19
- package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
- package/templates/firebase/login-redesign-preview.png +0 -0
package/lib/commands/new.js
CHANGED
|
@@ -44,8 +44,10 @@ const { generateApiProject } = require('../scaffold/backends/api/generator');
|
|
|
44
44
|
const { createProjectAndGetKeys, setupLinkedProject, checkLoggedIn, getOrgsList, getProjectsByOrg, getProjectKeys, classifyCreateError } = require('../scaffold/backends/supabase/deploy');
|
|
45
45
|
const { writeSupabaseGoogleAuthOptions, readSupabaseGoogleCredentials, getGoogleClientSecretViaGcloud, flutterfireConfigure, writeGoogleAuthOptions, ensureGoogleServiceInfoPlist, writeGoogleIosUrlScheme, writeGoogleIosUrlSchemeFromClientId } = require('../scaffold/shared/post-build');
|
|
46
46
|
const { toPackageName } = require('../scaffold/backends/firebase/tokens');
|
|
47
|
-
const { setupFromScratch, setupExistingProject, listBillingAccounts, listGcpOrganizations, checkGcloudAuth, getFirebaseAccount, getGcloudInstallInstructions, enableAuthProviders, ensureFirebaseAuthInitialized, registerDebugSha1 } = require('../scaffold/backends/firebase/setup-from-scratch');
|
|
47
|
+
const { setupFromScratch, setupExistingProject, listBillingAccounts, listGcpOrganizations, checkGcloudAuth, getFirebaseAccount, getGcloudInstallInstructions, enableAuthProviders, ensureFirebaseAuthInitialized, authorizeLocalhostForProject, registerDebugSha1 } = require('../scaffold/backends/firebase/setup-from-scratch');
|
|
48
48
|
const { enableAuthViaFirebaseCli } = require('../scaffold/backends/firebase/enable-auth-via-cli');
|
|
49
|
+
const { autoApplyAppleWebIfCached } = require('./apple-web');
|
|
50
|
+
const { autoApplyFacebookIfCached } = require('./facebook');
|
|
49
51
|
const { createFcmServiceAccountKey } = require('../scaffold/shared/fcm-service-account');
|
|
50
52
|
|
|
51
53
|
// Default region for creating Supabase projects via the API.
|
|
@@ -481,7 +483,7 @@ function printCreateFromScratchStatus(result, tr) {
|
|
|
481
483
|
}
|
|
482
484
|
}
|
|
483
485
|
|
|
484
|
-
function printSuccessCard(tr, answers, targetDir) {
|
|
486
|
+
function printSuccessCard(tr, answers, targetDir, appleWebStatus = null, facebookStatus = null) {
|
|
485
487
|
const folderName = path.basename(targetDir);
|
|
486
488
|
|
|
487
489
|
const lines = [];
|
|
@@ -509,6 +511,27 @@ function printSuccessCard(tr, answers, targetDir) {
|
|
|
509
511
|
}
|
|
510
512
|
}
|
|
511
513
|
|
|
514
|
+
if (appleWebStatus === 'configured') {
|
|
515
|
+
lines.push(paintLime(`✓ ${tr('new.success.appleWeb.configured')}`));
|
|
516
|
+
lines.push('');
|
|
517
|
+
} else if (appleWebStatus === 'pending') {
|
|
518
|
+
lines.push(kleur.yellow(`! ${tr('new.success.appleWeb.pending')}`));
|
|
519
|
+
lines.push('');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (facebookStatus === 'configured') {
|
|
523
|
+
lines.push(paintLime(`✓ ${tr('new.success.facebook.configured')}`));
|
|
524
|
+
lines.push('');
|
|
525
|
+
} else if (facebookStatus === 'pending') {
|
|
526
|
+
lines.push(kleur.yellow(`! ${tr('new.success.facebook.pending')}`));
|
|
527
|
+
lines.push('');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (answers.backend === 'api') {
|
|
531
|
+
lines.push(kleur.yellow(`! ${tr('new.success.api.serverContracts')}`));
|
|
532
|
+
lines.push('');
|
|
533
|
+
}
|
|
534
|
+
|
|
512
535
|
lines.push(kleur.bold(tr('new.success.nextSteps')));
|
|
513
536
|
lines.push('');
|
|
514
537
|
|
|
@@ -1827,6 +1850,15 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1827
1850
|
// Supabase setup runs in fcmOnly mode, which intentionally leaves Firebase
|
|
1828
1851
|
// Auth untouched, so initialize it here (idempotent) before the deploy.
|
|
1829
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
|
+
}
|
|
1830
1862
|
const cliResult = await enableAuthViaFirebaseCli({
|
|
1831
1863
|
projectDir: targetDir,
|
|
1832
1864
|
projectId: answers.firebaseProjectId,
|
|
@@ -2104,7 +2136,37 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
2104
2136
|
// APNs key (iOS push) is intentionally not mentioned here — it only becomes
|
|
2105
2137
|
// relevant when shipping to iOS, and the docs at kasy.dev/docs/apns explain it.
|
|
2106
2138
|
|
|
2107
|
-
|
|
2139
|
+
// Apple web: auto-configure if the developer saved credentials on a previous
|
|
2140
|
+
// project; otherwise it stays pending and the success card points to the command.
|
|
2141
|
+
let appleWebStatus = null;
|
|
2142
|
+
try {
|
|
2143
|
+
const aw = await autoApplyAppleWebIfCached(targetDir);
|
|
2144
|
+
if (aw.applied) {
|
|
2145
|
+
appleWebStatus = 'configured';
|
|
2146
|
+
ui.log.success(tr('new.appleWeb.autoConfigured'));
|
|
2147
|
+
} else if (aw.pending) {
|
|
2148
|
+
appleWebStatus = 'pending';
|
|
2149
|
+
}
|
|
2150
|
+
} catch (_) {
|
|
2151
|
+
// Non-fatal: web Apple is optional and can be configured later.
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// Facebook: auto-configure if it's enabled in the project and credentials were
|
|
2155
|
+
// saved on a previous project; otherwise pending and the card points to the command.
|
|
2156
|
+
let facebookStatus = null;
|
|
2157
|
+
try {
|
|
2158
|
+
const fb = await autoApplyFacebookIfCached(targetDir);
|
|
2159
|
+
if (fb.applied) {
|
|
2160
|
+
facebookStatus = 'configured';
|
|
2161
|
+
ui.log.success(tr('new.facebook.autoConfigured'));
|
|
2162
|
+
} else if (fb.pending) {
|
|
2163
|
+
facebookStatus = 'pending';
|
|
2164
|
+
}
|
|
2165
|
+
} catch (_) {
|
|
2166
|
+
// Non-fatal: Facebook is optional and can be configured later.
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
printSuccessCard(tr, answers, targetDir, appleWebStatus, facebookStatus);
|
|
2108
2170
|
|
|
2109
2171
|
ui.outro(kleur.bold(tr('new.success.title')));
|
|
2110
2172
|
}
|
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.34.0": {
|
|
3
|
+
"modules": {
|
|
4
|
+
"core": {
|
|
5
|
+
"pt": "Login com Facebook automatizado + Facebook na web (Firebase): novo comando `kasy facebook` guia os passos na Meta (abre o link), grava App ID + Client Token no iOS/Android e habilita o provedor no Firebase (Identity Toolkit) ou Supabase. No Firebase, o Facebook passa a funcionar na WEB (signInWithPopup) e o botão só aparece quando configurado (flag withFacebookWebSignin). Apple/Facebook na web no Supabase ficam como native-only (roadmap). Corrigido: o `kasy apple-web` agora liga o Apple web só no Firebase (não cria mais botão morto no Supabase). Credenciais ficam salvas e o `kasy new` aplica sozinho.",
|
|
6
|
+
"en": "Facebook Login automated + Facebook on web (Firebase): new `kasy facebook` command guides the Meta steps (opens the link), writes App ID + Client Token into iOS/Android and enables the provider on Firebase (Identity Toolkit) or Supabase. On Firebase, Facebook now works on the WEB (signInWithPopup) and the button only shows once configured (withFacebookWebSignin flag). Apple/Facebook on web for Supabase stay native-only (roadmap). Fixed: `kasy apple-web` now enables Apple web on Firebase only (no more dead button on Supabase). Credentials are cached and `kasy new` applies them automatically.",
|
|
7
|
+
"es": "Inicio de sesión con Facebook automatizado + Facebook en la web (Firebase): nuevo comando `kasy facebook` guía los pasos en Meta (abre el enlace), escribe App ID + Client Token en iOS/Android y habilita el proveedor en Firebase (Identity Toolkit) o Supabase. En Firebase, Facebook ahora funciona en la WEB (signInWithPopup) y el botón solo aparece cuando está configurado (flag withFacebookWebSignin). Apple/Facebook en la web para Supabase quedan como native-only (roadmap). Corregido: `kasy apple-web` ahora activa Apple web solo en Firebase (sin botón muerto en Supabase). Las credenciales se guardan y `kasy new` las aplica automáticamente."
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"1.33.0": {
|
|
12
|
+
"modules": {
|
|
13
|
+
"core": {
|
|
14
|
+
"pt": "Login com Apple na WEB agora é automatizável (backend Firebase): novo comando `kasy apple-web` grava o codeFlowConfig (Service ID + Team ID + Key ID + .p8) no provedor Apple do Firebase, que re-assina o secret sozinho (não expira), reaproveitando suas credenciais salvas. Projetos Firebase novos já nascem com Apple web se você já configurou antes. O botão Apple na web só aparece quando funciona de verdade (sem botão morto); `kasy doctor` mostra se falta configurar. No Supabase, Apple na web é roadmap.",
|
|
15
|
+
"en": "Apple Sign-In on the WEB is now automatable (Firebase backend): new `kasy apple-web` command writes the codeFlowConfig (Service ID + Team ID + Key ID + .p8) into the Firebase Apple provider, which re-signs the secret itself (never expires), reusing your saved credentials. New Firebase projects ship web Apple ready if you configured it before. The web Apple button only shows when it actually works (no dead button); `kasy doctor` reports if it's pending. On Supabase, Apple on web is roadmap.",
|
|
16
|
+
"es": "El inicio de sesión con Apple en la WEB ahora es automatizable (backend Firebase): nuevo comando `kasy apple-web` escribe el codeFlowConfig (Service ID + Team ID + Key ID + .p8) en el proveedor Apple de Firebase, que vuelve a firmar el secret solo (no expira), reutilizando tus credenciales guardadas. Los proyectos Firebase nuevos vienen con Apple web listo si ya lo configuraste antes. El botón de Apple en la web solo aparece cuando funciona de verdad (sin botón muerto); `kasy doctor` indica si falta configurar. En Supabase, Apple en la web es roadmap."
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"1.32.0": {
|
|
21
|
+
"modules": {
|
|
22
|
+
"core": {
|
|
23
|
+
"pt": "Proporção da web agora se adapta à largura real da janela: telas com escala alta do sistema (Windows em 125/150/175%) não ficam mais cortadas (sidebar/header), e o Mac continua igual. Ajuste só na web, nativo intocado.",
|
|
24
|
+
"en": "Web proportion now adapts to the real window width: high system-scale displays (Windows at 125/150/175%) no longer look cropped (sidebar/header), and Mac stays the same. Web-only, native untouched.",
|
|
25
|
+
"es": "La proporción en web ahora se adapta al ancho real de la ventana: pantallas con escala alta del sistema (Windows al 125/150/175%) ya no se ven recortadas (sidebar/header), y Mac queda igual. Solo en web, nativo intacto."
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
2
29
|
"1.31.12": {
|
|
3
30
|
"modules": {
|
|
4
31
|
"core": {
|
|
@@ -37,6 +37,7 @@ Ficam no `.vscode/launch.json` como variáveis de ambiente de build (`--dart-def
|
|
|
37
37
|
| `RC_ANDROID_PROD_KEY` | RevenueCat | Dashboard RevenueCat → Apps → Google Play (chave `goog_`, usada automaticamente em Android físico) |
|
|
38
38
|
| `SENTRY_DSN` | Sentry | Dashboard Sentry → Projeto → DSN |
|
|
39
39
|
| `MIXPANEL_TOKEN` | Mixpanel | Dashboard Mixpanel → Configurações → Token |
|
|
40
|
+
| `AI_CHAT_ENDPOINT` | IA Chat | URL do seu endpoint SSE de chat (ex.: `https://sua-api/ai-chat`) |
|
|
40
41
|
|
|
41
42
|
Para atualizar, edite o `.vscode/launch.json`.
|
|
42
43
|
|
|
@@ -157,8 +158,92 @@ Formato de uma conversa (o "última mensagem" é desnormalizado para a lista fic
|
|
|
157
158
|
```
|
|
158
159
|
|
|
159
160
|
Ao salvar uma mensagem, o servidor deve atualizar `updated_at`, `last_message_role` e
|
|
160
|
-
`last_message_content` da conversa.
|
|
161
|
-
|
|
161
|
+
`last_message_content` da conversa.
|
|
162
|
+
|
|
163
|
+
### Endpoint: AI Chat (resposta em streaming)
|
|
164
|
+
|
|
165
|
+
A resposta da IA é transmitida palavra a palavra via SSE por um endpoint separado,
|
|
166
|
+
cuja URL vem do `--dart-define=AI_CHAT_ENDPOINT=...` (quadro de credenciais acima).
|
|
167
|
+
Este endpoint **não persiste nada** — só faz o proxy para o provedor (OpenAI/Gemini)
|
|
168
|
+
e devolve o texto em stream. A chave do provedor fica **só no servidor**.
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
POST {AI_CHAT_ENDPOINT}
|
|
172
|
+
Auth: Authorization: Bearer <token> (enviado automaticamente pelo app)
|
|
173
|
+
Content-Type: application/json
|
|
174
|
+
body:
|
|
175
|
+
{
|
|
176
|
+
"message": "última mensagem do usuário",
|
|
177
|
+
"history": [ { "role": "user" | "assistant", "content": "..." } ]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
200 OK (text/event-stream)
|
|
181
|
+
→ devolva o texto da resposta em chunks (stream); o app concatena e renderiza
|
|
182
|
+
em tempo real. Mantenha a chave da IA (OPENAI/GEMINI) apenas no servidor.
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Se `AI_CHAT_ENDPOINT` não estiver definido, o chat mostra o estado "não configurado"
|
|
186
|
+
(o app não quebra). Referência pronta: a Edge Function `ai-chat` do backend Supabase.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Excluir conta
|
|
191
|
+
|
|
192
|
+
O app chama um endpoint para o usuário excluir a própria conta. **É obrigatório
|
|
193
|
+
para publicar na App Store e na Play Store**, então o seu backend precisa implementá-lo.
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
DELETE /users/me
|
|
197
|
+
Auth: Authorization: Bearer <token> (identifica o usuário; enviado pelo app)
|
|
198
|
+
|
|
199
|
+
O servidor DEVE:
|
|
200
|
+
1. Apagar o usuário do sistema de auth para que aquele login nunca mais entre.
|
|
201
|
+
2. Apagar em cascata TODOS os dados do usuário: perfil, devices/tokens de push,
|
|
202
|
+
conversas + mensagens de IA, votos de feature requests, assinaturas, avatar.
|
|
203
|
+
→ responda 2xx em caso de sucesso.
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Sem esse endpoint, a exclusão de conta falha silenciosamente (404/405) num projeto
|
|
207
|
+
API novo.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Notificações push (FCM)
|
|
212
|
+
|
|
213
|
+
Push **nativo (Android/iOS)** depende do seu servidor: o app registra o token do
|
|
214
|
+
device e o seu backend envia via **FCM HTTP v1**. Na **web**, push é no-op de propósito
|
|
215
|
+
(o app não registra token e não tenta enviar — só mostra as notificações que já existem).
|
|
216
|
+
|
|
217
|
+
A chave de Service Account do Firebase foi salva pelo `kasy new` em
|
|
218
|
+
`.kasy/fcm-service-account.json`. Carregue-a no servidor (ex.: variável
|
|
219
|
+
`FIREBASE_SERVICE_ACCOUNT_JSON`) e use-a para chamar a FCM HTTP v1. Referência pronta
|
|
220
|
+
de implementação: a Edge Function `send-push-notification` do backend Supabase.
|
|
221
|
+
|
|
222
|
+
### Endpoints: devices (tokens de push)
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
POST /users/{userId}/devices → registra/atualiza um device (body com token, platform, etc.)
|
|
226
|
+
PUT /devices/{deviceId} → atualiza um device existente
|
|
227
|
+
DELETE /devices/{deviceId} → remove um device
|
|
228
|
+
PATCH /users/{userId}/devices/{installationId}/touch → marca o device como ativo agora (last-seen)
|
|
229
|
+
POST /users/{userId}/devices/cleanup-stale → remove devices antigos/inválidos
|
|
230
|
+
DELETE /users/{userId}/devices → remove todos os devices do usuário (ex.: no logout)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Endpoints: notifications
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
GET /users/{userId}/notifications?page=&pageSize= → lista paginada, mais recente primeiro
|
|
237
|
+
PUT /users/{userId}/notifications/{id} → marca como lida
|
|
238
|
+
DELETE /users/{userId}/notifications/{id} → apaga uma notificação
|
|
239
|
+
GET /users/{userId}/notifications/unread → SSE: stream da contagem de não-lidas (alimenta a "bolinha")
|
|
240
|
+
POST /users/{userId}/notifications → cria/envia para UM usuário (body: title, body, image_url?, data.route?, type)
|
|
241
|
+
POST /notifications/broadcast → envia para TODOS (mesmo body)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Todas exigem `Authorization: Bearer <token>` (enviado automaticamente). Ao criar uma
|
|
245
|
+
notificação, o servidor persiste o registro **e** dispara o push via FCM para os devices
|
|
246
|
+
do destinatário.
|
|
162
247
|
|
|
163
248
|
---
|
|
164
249
|
|
package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart
CHANGED
|
@@ -3,6 +3,7 @@ import 'package:kasy_kit/core/data/api/http_client.dart';
|
|
|
3
3
|
import 'package:kasy_kit/core/data/entities/user_entity.dart';
|
|
4
4
|
import 'package:kasy_kit/features/authentication/api/authentication_api_interface.dart';
|
|
5
5
|
import 'package:kasy_kit/features/authentication/repositories/exceptions/authentication_exceptions.dart';
|
|
6
|
+
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
6
7
|
import 'package:flutter/services.dart' show PlatformException;
|
|
7
8
|
import 'package:dio/dio.dart';
|
|
8
9
|
import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
|
|
@@ -119,6 +120,15 @@ class HttpAuthenticationApi implements AuthenticationApi {
|
|
|
119
120
|
|
|
120
121
|
@override
|
|
121
122
|
Future<Credentials> signinWithGoogle() async {
|
|
123
|
+
// google_sign_in v7 authenticate() is unsupported on web (throws). The API
|
|
124
|
+
// backend is wire-it-yourself, so fail clearly instead of an opaque crash.
|
|
125
|
+
if (kIsWeb) {
|
|
126
|
+
throw UnimplementedError(
|
|
127
|
+
'Google sign-in on web is not wired for the API backend. Use '
|
|
128
|
+
'google_sign_in_web (renderButton) or a backend OAuth redirect, then '
|
|
129
|
+
'exchange the token with your API.',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
122
132
|
final googleSignIn = GoogleSignIn.instance;
|
|
123
133
|
await googleSignIn.initialize(
|
|
124
134
|
clientId: const String.fromEnvironment('GOOGLE_CLIENT_ID'),
|
|
@@ -153,6 +163,15 @@ class HttpAuthenticationApi implements AuthenticationApi {
|
|
|
153
163
|
|
|
154
164
|
@override
|
|
155
165
|
Future<Credentials> signinWithApple() async {
|
|
166
|
+
// Apple on web needs a paid Apple Service ID + secret and a backend token
|
|
167
|
+
// exchange; getAppleIDCredential crashes on web. The UI hides the Apple button
|
|
168
|
+
// on web; guard here too so a programmatic call fails clearly.
|
|
169
|
+
if (kIsWeb) {
|
|
170
|
+
throw UnimplementedError(
|
|
171
|
+
'Apple sign-in on web needs a paid Apple Service ID + secret and a backend '
|
|
172
|
+
'token exchange; it is not wired for the API backend.',
|
|
173
|
+
);
|
|
174
|
+
}
|
|
156
175
|
late final AuthorizationCredentialAppleID credential;
|
|
157
176
|
try {
|
|
158
177
|
credential = await SignInWithApple.getAppleIDCredential(
|
|
@@ -212,6 +231,14 @@ class HttpAuthenticationApi implements AuthenticationApi {
|
|
|
212
231
|
/// POST /auth/link-google {id_token, access_token} → Credentials
|
|
213
232
|
@override
|
|
214
233
|
Future<Credentials> signupFromAnonymousWithGoogle() async {
|
|
234
|
+
// See signinWithGoogle: authenticate() is unsupported on web.
|
|
235
|
+
if (kIsWeb) {
|
|
236
|
+
throw UnimplementedError(
|
|
237
|
+
'Google sign-in on web is not wired for the API backend. Use '
|
|
238
|
+
'google_sign_in_web (renderButton) or a backend OAuth redirect, then '
|
|
239
|
+
'exchange the token with your API.',
|
|
240
|
+
);
|
|
241
|
+
}
|
|
215
242
|
final googleSignIn = GoogleSignIn.instance;
|
|
216
243
|
await googleSignIn.initialize(
|
|
217
244
|
clientId: const String.fromEnvironment('GOOGLE_CLIENT_ID'),
|
|
@@ -242,6 +269,13 @@ class HttpAuthenticationApi implements AuthenticationApi {
|
|
|
242
269
|
/// POST /auth/link-apple {identity_token, authorization_code} → Credentials
|
|
243
270
|
@override
|
|
244
271
|
Future<Credentials> signupFromAnonymousWithApple() async {
|
|
272
|
+
// See signinWithApple: Apple on web is not wired for the API backend.
|
|
273
|
+
if (kIsWeb) {
|
|
274
|
+
throw UnimplementedError(
|
|
275
|
+
'Apple sign-in on web needs a paid Apple Service ID + secret and a backend '
|
|
276
|
+
'token exchange; it is not wired for the API backend.',
|
|
277
|
+
);
|
|
278
|
+
}
|
|
245
279
|
late final AuthorizationCredentialAppleID credential;
|
|
246
280
|
try {
|
|
247
281
|
credential = await SignInWithApple.getAppleIDCredential(
|
package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart
CHANGED
|
@@ -38,6 +38,7 @@ class StripeBackendApi {
|
|
|
38
38
|
String? successUrl,
|
|
39
39
|
String? cancelUrl,
|
|
40
40
|
String? locale,
|
|
41
|
+
bool? allowPromoCodes,
|
|
41
42
|
}) async {
|
|
42
43
|
final Response res = await _client.post(
|
|
43
44
|
'/stripe/checkout-session',
|
|
@@ -46,16 +47,25 @@ class StripeBackendApi {
|
|
|
46
47
|
if (successUrl != null) 'successUrl': successUrl,
|
|
47
48
|
if (cancelUrl != null) 'cancelUrl': cancelUrl,
|
|
48
49
|
if (locale != null) 'locale': locale,
|
|
50
|
+
if (allowPromoCodes != null) 'allowPromoCodes': allowPromoCodes,
|
|
49
51
|
},
|
|
50
52
|
);
|
|
51
53
|
return (res.data as Map)['url'] as String;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/// Create a Customer Portal session (manage / cancel) and return its URL.
|
|
55
|
-
|
|
57
|
+
/// Pass [planSwitching] = true to auto-configure the portal with
|
|
58
|
+
/// upgrade/downgrade support (no manual Stripe dashboard setup needed).
|
|
59
|
+
Future<String> createPortalSession({
|
|
60
|
+
String? returnUrl,
|
|
61
|
+
bool? planSwitching,
|
|
62
|
+
}) async {
|
|
56
63
|
final Response res = await _client.post(
|
|
57
64
|
'/stripe/portal-session',
|
|
58
|
-
data: {
|
|
65
|
+
data: {
|
|
66
|
+
if (returnUrl != null) 'returnUrl': returnUrl,
|
|
67
|
+
if (planSwitching != null) 'planSwitching': planSwitching,
|
|
68
|
+
},
|
|
59
69
|
);
|
|
60
70
|
return (res.data as Map)['url'] as String;
|
|
61
71
|
}
|
|
@@ -743,6 +743,189 @@ async function ensureLocalhostAuthorizedDomains(projectId, token) {
|
|
|
743
743
|
return { ok: true, added: missing };
|
|
744
744
|
}
|
|
745
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Authorize localhost (and 127.0.0.1) on a project's Firebase Auth config,
|
|
748
|
+
* fetching the gcloud access token internally. Exported so callers that don't
|
|
749
|
+
* run the full setup (e.g. the Supabase backend, which sets up Firebase in
|
|
750
|
+
* fcmOnly mode and never reaches enableAuthProviders) can still authorize the
|
|
751
|
+
* domains the web Google popup needs. Best-effort: returns { ok, error } and
|
|
752
|
+
* never throws.
|
|
753
|
+
*
|
|
754
|
+
* @param {string} projectId
|
|
755
|
+
* @returns {{ ok: boolean, added?: string[], error?: string }}
|
|
756
|
+
*/
|
|
757
|
+
async function authorizeLocalhostForProject(projectId) {
|
|
758
|
+
let token;
|
|
759
|
+
try {
|
|
760
|
+
token = await getAccessToken();
|
|
761
|
+
} catch (_) {
|
|
762
|
+
return { ok: false, error: 'Could not get access token' };
|
|
763
|
+
}
|
|
764
|
+
return ensureLocalhostAuthorizedDomains(projectId, token);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Configure Apple Sign-In on Firebase for the WEB (and the OAuth code flow) by
|
|
769
|
+
* writing the Apple provider's codeFlowConfig (Service ID + Team ID + Key ID +
|
|
770
|
+
* `.p8`) via the Identity Toolkit Admin v2 API. Once stored, Firebase re-signs the
|
|
771
|
+
* short-lived client secret itself, so it never expires.
|
|
772
|
+
*
|
|
773
|
+
* Existing bundleIds (used by the native iOS flow) are preserved and the project's
|
|
774
|
+
* own bundleId is merged in, so configuring web never breaks native.
|
|
775
|
+
*
|
|
776
|
+
* @param {object} opts
|
|
777
|
+
* @param {string} opts.projectId
|
|
778
|
+
* @param {string} opts.serviceId - Apple Service ID (becomes the provider clientId)
|
|
779
|
+
* @param {string} opts.teamId
|
|
780
|
+
* @param {string} opts.keyId
|
|
781
|
+
* @param {string} opts.privateKey - PEM contents of the .p8
|
|
782
|
+
* @param {string} [opts.bundleId] - app bundle id to keep allowed for native sign-in
|
|
783
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
784
|
+
*/
|
|
785
|
+
async function configureFirebaseAppleWeb({ projectId, serviceId, teamId, keyId, privateKey, bundleId }) {
|
|
786
|
+
if (!serviceId || !teamId || !keyId || !privateKey) {
|
|
787
|
+
return { ok: false, error: 'serviceId, teamId, keyId and privateKey are required' };
|
|
788
|
+
}
|
|
789
|
+
let token;
|
|
790
|
+
try {
|
|
791
|
+
token = await getAccessToken();
|
|
792
|
+
} catch (_) {
|
|
793
|
+
return { ok: false, error: 'Could not get access token' };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const headers = {
|
|
797
|
+
Authorization: `Bearer ${token}`,
|
|
798
|
+
'Content-Type': 'application/json',
|
|
799
|
+
'X-Goog-User-Project': projectId,
|
|
800
|
+
};
|
|
801
|
+
const base = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/defaultSupportedIdpConfigs`;
|
|
802
|
+
|
|
803
|
+
// Read the current Apple config (if any) so we preserve the native bundleIds.
|
|
804
|
+
let existing = null;
|
|
805
|
+
try {
|
|
806
|
+
const getRes = await fetch(`${base}/apple.com`, { headers });
|
|
807
|
+
if (getRes.ok) existing = await getRes.json();
|
|
808
|
+
} catch (_) {
|
|
809
|
+
// No existing config (or transient) — we'll create it below.
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const bundleIds = Array.from(
|
|
813
|
+
new Set([...(existing?.appleSignInConfig?.bundleIds || []), bundleId].filter(Boolean)),
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
const body = {
|
|
817
|
+
name: `projects/${projectId}/defaultSupportedIdpConfigs/apple.com`,
|
|
818
|
+
enabled: true,
|
|
819
|
+
clientId: serviceId,
|
|
820
|
+
appleSignInConfig: {
|
|
821
|
+
codeFlowConfig: { teamId, keyId, privateKey },
|
|
822
|
+
...(bundleIds.length ? { bundleIds } : {}),
|
|
823
|
+
},
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
try {
|
|
827
|
+
if (existing) {
|
|
828
|
+
const res = await fetch(`${base}/apple.com?updateMask=enabled,clientId,appleSignInConfig`, {
|
|
829
|
+
method: 'PATCH',
|
|
830
|
+
headers,
|
|
831
|
+
body: JSON.stringify(body),
|
|
832
|
+
});
|
|
833
|
+
if (!res.ok) {
|
|
834
|
+
const text = await res.text();
|
|
835
|
+
return { ok: false, error: `PATCH failed (${res.status}): ${text.slice(0, 200)}` };
|
|
836
|
+
}
|
|
837
|
+
} else {
|
|
838
|
+
const res = await fetch(`${base}?idpId=apple.com`, {
|
|
839
|
+
method: 'POST',
|
|
840
|
+
headers,
|
|
841
|
+
body: JSON.stringify(body),
|
|
842
|
+
});
|
|
843
|
+
if (!res.ok) {
|
|
844
|
+
const text = await res.text();
|
|
845
|
+
// Raced with another writer — fall back to PATCH.
|
|
846
|
+
if (res.status === 409 || text.includes('ALREADY_EXISTS')) {
|
|
847
|
+
const patchRes = await fetch(`${base}/apple.com?updateMask=enabled,clientId,appleSignInConfig`, {
|
|
848
|
+
method: 'PATCH',
|
|
849
|
+
headers,
|
|
850
|
+
body: JSON.stringify(body),
|
|
851
|
+
});
|
|
852
|
+
if (!patchRes.ok) {
|
|
853
|
+
const t2 = await patchRes.text();
|
|
854
|
+
return { ok: false, error: `PATCH failed (${patchRes.status}): ${t2.slice(0, 200)}` };
|
|
855
|
+
}
|
|
856
|
+
} else {
|
|
857
|
+
return { ok: false, error: `POST failed (${res.status}): ${text.slice(0, 200)}` };
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
} catch (err) {
|
|
862
|
+
return { ok: false, error: `Network error: ${err.message}` };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return { ok: true };
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Enable the Facebook provider on Firebase via the Identity Toolkit Admin v2 API.
|
|
870
|
+
* Needs the Meta App ID (clientId) + App Secret (clientSecret). The native App ID /
|
|
871
|
+
* Client Token live in Info.plist / strings.xml and are written separately.
|
|
872
|
+
*
|
|
873
|
+
* @param {object} opts - { projectId, appId, appSecret }
|
|
874
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
875
|
+
*/
|
|
876
|
+
async function configureFirebaseFacebook({ projectId, appId, appSecret }) {
|
|
877
|
+
if (!appId || !appSecret) {
|
|
878
|
+
return { ok: false, error: 'appId and appSecret are required' };
|
|
879
|
+
}
|
|
880
|
+
let token;
|
|
881
|
+
try {
|
|
882
|
+
token = await getAccessToken();
|
|
883
|
+
} catch (_) {
|
|
884
|
+
return { ok: false, error: 'Could not get access token' };
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const headers = {
|
|
888
|
+
Authorization: `Bearer ${token}`,
|
|
889
|
+
'Content-Type': 'application/json',
|
|
890
|
+
'X-Goog-User-Project': projectId,
|
|
891
|
+
};
|
|
892
|
+
const base = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/defaultSupportedIdpConfigs`;
|
|
893
|
+
const body = {
|
|
894
|
+
name: `projects/${projectId}/defaultSupportedIdpConfigs/facebook.com`,
|
|
895
|
+
enabled: true,
|
|
896
|
+
clientId: appId,
|
|
897
|
+
clientSecret: appSecret,
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
try {
|
|
901
|
+
let res = await fetch(`${base}?idpId=facebook.com`, {
|
|
902
|
+
method: 'POST',
|
|
903
|
+
headers,
|
|
904
|
+
body: JSON.stringify(body),
|
|
905
|
+
});
|
|
906
|
+
if (!res.ok) {
|
|
907
|
+
const text = await res.text();
|
|
908
|
+
if (res.status === 409 || text.includes('ALREADY_EXISTS')) {
|
|
909
|
+
res = await fetch(`${base}/facebook.com?updateMask=enabled,clientId,clientSecret`, {
|
|
910
|
+
method: 'PATCH',
|
|
911
|
+
headers,
|
|
912
|
+
body: JSON.stringify(body),
|
|
913
|
+
});
|
|
914
|
+
if (!res.ok) {
|
|
915
|
+
const t2 = await res.text();
|
|
916
|
+
return { ok: false, error: `PATCH failed (${res.status}): ${t2.slice(0, 200)}` };
|
|
917
|
+
}
|
|
918
|
+
} else {
|
|
919
|
+
return { ok: false, error: `POST failed (${res.status}): ${text.slice(0, 200)}` };
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
} catch (err) {
|
|
923
|
+
return { ok: false, error: `Network error: ${err.message}` };
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return { ok: true };
|
|
927
|
+
}
|
|
928
|
+
|
|
746
929
|
/**
|
|
747
930
|
* Initialize Firebase Auth (Identity Platform) for a project. Brand-new projects
|
|
748
931
|
* have no auth config, so any Admin v2 operation (PATCH /config, or the Firebase
|
|
@@ -1277,6 +1460,9 @@ module.exports = {
|
|
|
1277
1460
|
enableAuthProviders,
|
|
1278
1461
|
ensureFirebaseAuthInitialized,
|
|
1279
1462
|
ensureLocalhostAuthorizedDomains,
|
|
1463
|
+
authorizeLocalhostForProject,
|
|
1464
|
+
configureFirebaseAppleWeb,
|
|
1465
|
+
configureFirebaseFacebook,
|
|
1280
1466
|
listBillingAccounts,
|
|
1281
1467
|
listGcpOrganizations,
|
|
1282
1468
|
checkGcloudAuth,
|
|
@@ -15,6 +15,7 @@ const path = require('node:path');
|
|
|
15
15
|
const os = require('node:os');
|
|
16
16
|
const fs = require('fs-extra');
|
|
17
17
|
const { augmentedEnv } = require('../../../utils/env-tools');
|
|
18
|
+
const { signAppleClientSecret } = require('../../../utils/apple-web');
|
|
18
19
|
|
|
19
20
|
const execAsync = promisify(exec);
|
|
20
21
|
|
|
@@ -407,6 +408,95 @@ async function enableAppleSignIn(projectRef, bundleId) {
|
|
|
407
408
|
return { ok: false, error: result.data.message || JSON.stringify(result.data) };
|
|
408
409
|
}
|
|
409
410
|
|
|
411
|
+
/**
|
|
412
|
+
* GET the current Supabase auth config (read-only). Used to merge values we must
|
|
413
|
+
* not clobber (e.g. the native bundle id already in external_apple_client_id).
|
|
414
|
+
*/
|
|
415
|
+
async function getSupabaseAuthConfig(projectRef, token) {
|
|
416
|
+
try {
|
|
417
|
+
const res = await fetch(`https://api.supabase.com/v1/projects/${projectRef}/config/auth`, {
|
|
418
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
419
|
+
});
|
|
420
|
+
if (!res.ok) return null;
|
|
421
|
+
return await res.json();
|
|
422
|
+
} catch {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Enable Apple Sign-In on the WEB for Supabase.
|
|
429
|
+
*
|
|
430
|
+
* Unlike Firebase (which stores the .p8 and re-signs), Supabase stores a static,
|
|
431
|
+
* pre-signed client secret JWT that expires every ~6 months. We sign it here with
|
|
432
|
+
* the developer's `.p8` and write it as external_apple_secret. The Service ID is
|
|
433
|
+
* added to external_apple_client_id (the audience list) alongside the native bundle
|
|
434
|
+
* id, so both native iOS and the web OAuth flow validate.
|
|
435
|
+
*
|
|
436
|
+
* @param {string} projectRef
|
|
437
|
+
* @param {object} opts - { serviceId, teamId, keyId, privateKey, bundleId? }
|
|
438
|
+
* @returns {{ ok: boolean, error?: string, expiresAt?: number }}
|
|
439
|
+
*/
|
|
440
|
+
async function enableAppleWebSignIn(projectRef, { serviceId, teamId, keyId, privateKey, bundleId } = {}) {
|
|
441
|
+
if (!serviceId || !teamId || !keyId || !privateKey) {
|
|
442
|
+
return { ok: false, error: 'serviceId, teamId, keyId and privateKey are required' };
|
|
443
|
+
}
|
|
444
|
+
const token = await getSupabaseAccessToken();
|
|
445
|
+
if (!token) return { ok: false, error: 'Could not retrieve Supabase access token' };
|
|
446
|
+
|
|
447
|
+
let secret;
|
|
448
|
+
let expiresAt;
|
|
449
|
+
try {
|
|
450
|
+
({ token: secret, expiresAt } = signAppleClientSecret({ serviceId, teamId, keyId, privateKey }));
|
|
451
|
+
} catch (err) {
|
|
452
|
+
return { ok: false, error: err.message };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Merge the Service ID into the existing audience list without dropping the
|
|
456
|
+
// native bundle id the CLI set at creation.
|
|
457
|
+
const current = await getSupabaseAuthConfig(projectRef, token);
|
|
458
|
+
const existingIds = String(current?.external_apple_client_id || '')
|
|
459
|
+
.split(',')
|
|
460
|
+
.map((s) => s.trim())
|
|
461
|
+
.filter(Boolean);
|
|
462
|
+
const clientIds = Array.from(new Set([...existingIds, bundleId, serviceId].filter(Boolean))).join(',');
|
|
463
|
+
|
|
464
|
+
const result = await patchAuthConfig(projectRef, token, {
|
|
465
|
+
external_apple_enabled: true,
|
|
466
|
+
external_apple_client_id: clientIds,
|
|
467
|
+
external_apple_secret: secret,
|
|
468
|
+
});
|
|
469
|
+
if (!result.ok) return { ok: false, error: result.error };
|
|
470
|
+
if (result.data.external_apple_enabled === true) return { ok: true, expiresAt };
|
|
471
|
+
return { ok: false, error: result.data.message || JSON.stringify(result.data) };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Enable the Facebook provider on Supabase via the Management API.
|
|
476
|
+
* Needs the Meta App ID (client_id) + App Secret (secret). The native App ID /
|
|
477
|
+
* Client Token live in Info.plist / strings.xml and are written separately.
|
|
478
|
+
*
|
|
479
|
+
* @param {string} projectRef
|
|
480
|
+
* @param {object} opts - { appId, appSecret }
|
|
481
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
482
|
+
*/
|
|
483
|
+
async function enableFacebookSignIn(projectRef, { appId, appSecret } = {}) {
|
|
484
|
+
if (!appId || !appSecret) {
|
|
485
|
+
return { ok: false, error: 'appId and appSecret are required' };
|
|
486
|
+
}
|
|
487
|
+
const token = await getSupabaseAccessToken();
|
|
488
|
+
if (!token) return { ok: false, error: 'Could not retrieve Supabase access token' };
|
|
489
|
+
|
|
490
|
+
const result = await patchAuthConfig(projectRef, token, {
|
|
491
|
+
external_facebook_enabled: true,
|
|
492
|
+
external_facebook_client_id: appId,
|
|
493
|
+
external_facebook_secret: appSecret,
|
|
494
|
+
});
|
|
495
|
+
if (!result.ok) return { ok: false, error: result.error };
|
|
496
|
+
if (result.data.external_facebook_enabled === true) return { ok: true };
|
|
497
|
+
return { ok: false, error: result.data.message || JSON.stringify(result.data) };
|
|
498
|
+
}
|
|
499
|
+
|
|
410
500
|
/**
|
|
411
501
|
* Configure auth settings via Supabase Management API:
|
|
412
502
|
* - Enable anonymous sign-in
|
|
@@ -717,6 +807,8 @@ module.exports = {
|
|
|
717
807
|
enableAnonymousSignIn,
|
|
718
808
|
enableGoogleSignIn,
|
|
719
809
|
enableAppleSignIn,
|
|
810
|
+
enableAppleWebSignIn,
|
|
811
|
+
enableFacebookSignIn,
|
|
720
812
|
checkLoggedIn,
|
|
721
813
|
getOrgsList,
|
|
722
814
|
getProjectsByOrg,
|