kasy-cli 1.32.0 → 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 +50 -2
- package/lib/scaffold/CHANGELOG.json +18 -0
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +164 -0
- package/lib/scaffold/backends/supabase/deploy.js +92 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +10 -0
- 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 +62 -0
- package/lib/utils/i18n/messages-es.js +62 -0
- package/lib/utils/i18n/messages-pt.js +62 -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 +2 -2
- package/templates/firebase/docs/auth-setup.es.md +2 -2
- package/templates/firebase/docs/auth-setup.pt.md +2 -2
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_app_bar.dart +4 -1
- package/templates/firebase/lib/components/kasy_screen.dart +114 -0
- package/templates/firebase/lib/components/kasy_toast.dart +39 -70
- 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/rating/widgets/review_popup.dart +46 -124
- 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/authentication/api/authentication_api.dart +61 -0
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +21 -15
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +20 -14
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -2
- package/templates/firebase/lib/features/home/home_components_page.dart +7 -1
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +32 -0
- 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/notification_tile.dart +77 -126
- 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/ui/components/admin/admin_page.dart +17 -8
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
- package/templates/firebase/lib/i18n/en.i18n.json +5 -4
- package/templates/firebase/lib/i18n/es.i18n.json +5 -4
- package/templates/firebase/lib/i18n/pt.i18n.json +5 -4
- package/templates/firebase/lib/router.dart +2 -0
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/tool/design_check.dart +152 -0
- package/templates/firebase/assets/images/review.png +0 -0
- package/templates/firebase/assets/images/update.png +0 -0
- package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
|
@@ -1,4 +1,22 @@
|
|
|
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
|
+
},
|
|
2
20
|
"1.32.0": {
|
|
3
21
|
"modules": {
|
|
4
22
|
"core": {
|
|
@@ -764,6 +764,168 @@ async function authorizeLocalhostForProject(projectId) {
|
|
|
764
764
|
return ensureLocalhostAuthorizedDomains(projectId, token);
|
|
765
765
|
}
|
|
766
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
|
+
|
|
767
929
|
/**
|
|
768
930
|
* Initialize Firebase Auth (Identity Platform) for a project. Brand-new projects
|
|
769
931
|
* have no auth config, so any Admin v2 operation (PATCH /config, or the Firebase
|
|
@@ -1299,6 +1461,8 @@ module.exports = {
|
|
|
1299
1461
|
ensureFirebaseAuthInitialized,
|
|
1300
1462
|
ensureLocalhostAuthorizedDomains,
|
|
1301
1463
|
authorizeLocalhostForProject,
|
|
1464
|
+
configureFirebaseAppleWeb,
|
|
1465
|
+
configureFirebaseFacebook,
|
|
1302
1466
|
listBillingAccounts,
|
|
1303
1467
|
listGcpOrganizations,
|
|
1304
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,
|
package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart
CHANGED
|
@@ -192,6 +192,11 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
|
|
|
192
192
|
|
|
193
193
|
@override
|
|
194
194
|
Future<Credentials> signinWithFacebook() async {
|
|
195
|
+
// Facebook on web for Supabase is not wired yet (roadmap); the button is hidden
|
|
196
|
+
// on web, so this is a defensive guard.
|
|
197
|
+
if (kIsWeb) {
|
|
198
|
+
throw ApiError(code: 501, message: 'Facebook sign-in on web is not supported on Supabase.');
|
|
199
|
+
}
|
|
195
200
|
final loginResult = await FacebookAuth.instance.login(
|
|
196
201
|
permissions: ['email', 'public_profile'],
|
|
197
202
|
);
|
|
@@ -439,6 +444,11 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
|
|
|
439
444
|
|
|
440
445
|
@override
|
|
441
446
|
Future<Credentials> signupFromAnonymousWithFacebook() async {
|
|
447
|
+
// Facebook on web for Supabase is not wired yet (roadmap); the button is hidden
|
|
448
|
+
// on web, so this is a defensive guard.
|
|
449
|
+
if (kIsWeb) {
|
|
450
|
+
throw ApiError(code: 501, message: 'Facebook sign-in on web is not supported on Supabase.');
|
|
451
|
+
}
|
|
442
452
|
final loginResult = await FacebookAuth.instance.login(
|
|
443
453
|
permissions: ['email', 'public_profile'],
|
|
444
454
|
);
|
|
@@ -340,6 +340,7 @@ async function writeRouter(projectDir, modules, packageName, defaultPaywall = 'b
|
|
|
340
340
|
lines.push(`import 'package:flutter_riverpod/flutter_riverpod.dart';`);
|
|
341
341
|
lines.push(`import 'package:go_router/go_router.dart';`);
|
|
342
342
|
lines.push(`import 'package:${pkg}/core/bottom_menu/bottom_menu.dart';`);
|
|
343
|
+
lines.push(`import 'package:${pkg}/core/chrome/chrome_visibility.dart';`);
|
|
343
344
|
if (withAnalytics) {
|
|
344
345
|
lines.push(`import 'package:${pkg}/core/data/api/analytics_api.dart';`);
|
|
345
346
|
}
|
|
@@ -417,6 +418,7 @@ async function writeRouter(projectDir, modules, packageName, defaultPaywall = 'b
|
|
|
417
418
|
if (withAnalytics) {
|
|
418
419
|
lines.push(` AnalyticsObserver(analyticsApi: MixpanelAnalyticsApi.instance()),`);
|
|
419
420
|
}
|
|
421
|
+
lines.push(` KasyChromeVisibilityObserver(),`);
|
|
420
422
|
lines.push(` ...?observers,`);
|
|
421
423
|
lines.push(` ],`);
|
|
422
424
|
lines.push(` routes: [`);
|
|
@@ -624,10 +626,15 @@ async function writeFeaturesConfig(projectDir, modules, answers = {}, language =
|
|
|
624
626
|
const withStripe = modules.includes('stripe');
|
|
625
627
|
const withLocalReminders = modules.includes('local_reminders');
|
|
626
628
|
const withWeb = modules.includes('web');
|
|
627
|
-
// Apple sign-in on web
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
|
|
629
|
+
// Apple sign-in on web needs a Service ID + signed secret that don't exist until
|
|
630
|
+
// the developer configures it (`kasy apple-web`). Until then, showing the button
|
|
631
|
+
// means a dead button on web, so it ships false on every backend and the command
|
|
632
|
+
// flips it to true once web Apple actually works. Native always shows it.
|
|
633
|
+
const withAppleWebSignin = false;
|
|
634
|
+
// Facebook sign-in on web works on the Firebase backend (signInWithPopup) after
|
|
635
|
+
// `kasy facebook`; on Supabase the web flow isn't wired yet (roadmap). Ships false
|
|
636
|
+
// on every backend (the command flips it to true on Firebase). Native always shows.
|
|
637
|
+
const withFacebookWebSignin = false;
|
|
631
638
|
|
|
632
639
|
const f = getStrings(language).features;
|
|
633
640
|
const content = `${f.comment1}
|
|
@@ -644,9 +651,14 @@ const bool withStripePromoCodes = true;
|
|
|
644
651
|
// When true, the Stripe Customer Portal lets subscribers switch plans (upgrade / downgrade).
|
|
645
652
|
const bool withStripePlanSwitching = true;
|
|
646
653
|
const bool withLocalReminders = ${withLocalReminders};
|
|
647
|
-
// Apple sign-in on web:
|
|
648
|
-
//
|
|
654
|
+
// Apple sign-in on web: ships false until configured with \`kasy apple-web\` (needs a
|
|
655
|
+
// paid Apple Service ID + signed secret). The command flips this to true once web
|
|
656
|
+
// Apple actually works, so the button never appears dead. Native always shows it.
|
|
649
657
|
const bool withAppleWebSignin = ${withAppleWebSignin};
|
|
658
|
+
// Facebook sign-in on web: ships false until configured with \`kasy facebook\` on the
|
|
659
|
+
// Firebase backend (signInWithPopup). On Supabase the web flow is roadmap, so it stays
|
|
660
|
+
// false there. Native (iOS/Android) always shows the Facebook button.
|
|
661
|
+
const bool withFacebookWebSignin = ${withFacebookWebSignin};
|
|
650
662
|
${f.comment3}
|
|
651
663
|
${f.comment4}
|
|
652
664
|
${f.comment5}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple "Sign in with Apple" WEB support helpers.
|
|
3
|
+
*
|
|
4
|
+
* The native iOS/macOS flow needs no secret — the CLI already enables it on both
|
|
5
|
+
* backends at project creation. The WEB (and Android, which we hide) flow uses
|
|
6
|
+
* Apple's OAuth, which requires a Service ID + a client secret that is a short-lived
|
|
7
|
+
* JWT signed with the developer's `.p8` private key.
|
|
8
|
+
*
|
|
9
|
+
* Apple offers NO API to create the Service ID or the `.p8` key — those stay manual
|
|
10
|
+
* on developer.apple.com (done once per Apple account). What we CAN automate is
|
|
11
|
+
* taking the four inputs the developer already created and pushing them to the
|
|
12
|
+
* backend:
|
|
13
|
+
* - Firebase: store Service ID + Team ID + Key ID + `.p8` in the Apple provider
|
|
14
|
+
* (Firebase re-signs the JWT itself, so it never expires).
|
|
15
|
+
* - Supabase: sign the JWT here and store it as external_apple_secret (Supabase
|
|
16
|
+
* cannot re-sign, so this expires every 6 months and must be regenerated).
|
|
17
|
+
*
|
|
18
|
+
* This module: (1) signs the Apple client-secret JWT with node:crypto (no extra
|
|
19
|
+
* dependency), and (2) caches the four inputs in ~/.kasy/apple-web.json so future
|
|
20
|
+
* projects (and the 6-month Supabase renewal) configure web Apple without re-asking.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const crypto = require('node:crypto');
|
|
26
|
+
const fs = require('fs-extra');
|
|
27
|
+
|
|
28
|
+
// Apple caps the client-secret JWT lifetime at 6 months. We sign for 180 days to
|
|
29
|
+
// stay safely under the limit. Firebase ignores this (it re-signs); Supabase stores
|
|
30
|
+
// the JWT verbatim, so this is the value behind its "expires every 6 months" notice.
|
|
31
|
+
const APPLE_SECRET_MAX_SECONDS = 180 * 24 * 60 * 60;
|
|
32
|
+
|
|
33
|
+
const CONFIG_DIR = path.join(os.homedir(), '.kasy');
|
|
34
|
+
const APPLE_WEB_CONFIG_PATH = path.join(CONFIG_DIR, 'apple-web.json');
|
|
35
|
+
|
|
36
|
+
/** base64url-encode a string or Buffer (no padding, URL-safe alphabet). */
|
|
37
|
+
function base64url(input) {
|
|
38
|
+
return Buffer.from(input)
|
|
39
|
+
.toString('base64')
|
|
40
|
+
.replace(/=+$/g, '')
|
|
41
|
+
.replace(/\+/g, '-')
|
|
42
|
+
.replace(/\//g, '_');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Normalize a `.p8` private key that may arrive with literal "\n" sequences
|
|
47
|
+
* (common when pasted from a one-line env var) into real newlines so
|
|
48
|
+
* crypto.createPrivateKey can parse the PEM.
|
|
49
|
+
*/
|
|
50
|
+
function normalizePrivateKey(privateKey) {
|
|
51
|
+
const key = String(privateKey || '').trim();
|
|
52
|
+
if (key.includes('-----BEGIN') && !key.includes('\n') && key.includes('\\n')) {
|
|
53
|
+
return key.replace(/\\n/g, '\n');
|
|
54
|
+
}
|
|
55
|
+
return key;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sign the Apple "Sign in with Apple" client secret (an ES256 JWT).
|
|
60
|
+
*
|
|
61
|
+
* @param {object} opts
|
|
62
|
+
* @param {string} opts.serviceId - Apple Service ID (the OAuth client_id), e.g. com.acme.app.signin
|
|
63
|
+
* @param {string} opts.teamId - Apple Developer Team ID
|
|
64
|
+
* @param {string} opts.keyId - Key ID of the .p8
|
|
65
|
+
* @param {string} opts.privateKey - PEM contents of the .p8 (PKCS#8 EC P-256)
|
|
66
|
+
* @param {number} [opts.expiresInSeconds] - lifetime; clamped to Apple's 6-month max
|
|
67
|
+
* @returns {{ token: string, issuedAt: number, expiresAt: number }}
|
|
68
|
+
*/
|
|
69
|
+
function signAppleClientSecret({ serviceId, teamId, keyId, privateKey, expiresInSeconds = APPLE_SECRET_MAX_SECONDS } = {}) {
|
|
70
|
+
if (!serviceId) throw new Error('serviceId (Apple Service ID) is required');
|
|
71
|
+
if (!teamId) throw new Error('teamId (Apple Team ID) is required');
|
|
72
|
+
if (!keyId) throw new Error('keyId is required');
|
|
73
|
+
if (!privateKey) throw new Error('privateKey (.p8 contents) is required');
|
|
74
|
+
|
|
75
|
+
const issuedAt = Math.floor(Date.now() / 1000);
|
|
76
|
+
const expiresAt = issuedAt + Math.min(expiresInSeconds, APPLE_SECRET_MAX_SECONDS);
|
|
77
|
+
|
|
78
|
+
const header = { alg: 'ES256', kid: keyId };
|
|
79
|
+
const payload = {
|
|
80
|
+
iss: teamId,
|
|
81
|
+
iat: issuedAt,
|
|
82
|
+
exp: expiresAt,
|
|
83
|
+
aud: 'https://appleid.apple.com',
|
|
84
|
+
sub: serviceId,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const signingInput = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(payload))}`;
|
|
88
|
+
|
|
89
|
+
let keyObject;
|
|
90
|
+
try {
|
|
91
|
+
keyObject = crypto.createPrivateKey(normalizePrivateKey(privateKey));
|
|
92
|
+
} catch (err) {
|
|
93
|
+
throw new Error(`Invalid Apple .p8 private key: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ES256 JWTs need the raw r||s signature (IEEE P1363), not DER.
|
|
97
|
+
const signature = crypto.sign('sha256', Buffer.from(signingInput), {
|
|
98
|
+
key: keyObject,
|
|
99
|
+
dsaEncoding: 'ieee-p1363',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return { token: `${signingInput}.${base64url(signature)}`, issuedAt, expiresAt };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Load cached Apple web credentials from ~/.kasy/apple-web.json.
|
|
107
|
+
* Returns null if absent or incomplete.
|
|
108
|
+
*/
|
|
109
|
+
async function loadAppleWebCreds() {
|
|
110
|
+
try {
|
|
111
|
+
if (!(await fs.pathExists(APPLE_WEB_CONFIG_PATH))) return null;
|
|
112
|
+
const data = await fs.readJson(APPLE_WEB_CONFIG_PATH);
|
|
113
|
+
if (!data || !data.serviceId || !data.teamId || !data.keyId || !data.privateKey) return null;
|
|
114
|
+
return data;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Persist Apple web credentials to ~/.kasy/apple-web.json with 0600 perms so the
|
|
122
|
+
* 6-month Supabase renewal and future projects can reuse them without re-asking.
|
|
123
|
+
*/
|
|
124
|
+
async function saveAppleWebCreds({ serviceId, teamId, keyId, privateKey }) {
|
|
125
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
126
|
+
await fs.writeJson(
|
|
127
|
+
APPLE_WEB_CONFIG_PATH,
|
|
128
|
+
{ serviceId, teamId, keyId, privateKey: normalizePrivateKey(privateKey) },
|
|
129
|
+
{ spaces: 2 },
|
|
130
|
+
);
|
|
131
|
+
// Best effort: restrict to the owner (no-op / unsupported on some Windows setups).
|
|
132
|
+
try {
|
|
133
|
+
await fs.chmod(APPLE_WEB_CONFIG_PATH, 0o600);
|
|
134
|
+
} catch {
|
|
135
|
+
/* ignore */
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
APPLE_SECRET_MAX_SECONDS,
|
|
141
|
+
APPLE_WEB_CONFIG_PATH,
|
|
142
|
+
base64url,
|
|
143
|
+
normalizePrivateKey,
|
|
144
|
+
signAppleClientSecret,
|
|
145
|
+
loadAppleWebCreds,
|
|
146
|
+
saveAppleWebCreds,
|
|
147
|
+
};
|