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
|
@@ -73,6 +73,21 @@ abstract class AuthenticationApi implements OnStartService {
|
|
|
73
73
|
|
|
74
74
|
/// Returns the display name of the current user, or null.
|
|
75
75
|
Future<String?> getCurrentUserDisplayName();
|
|
76
|
+
|
|
77
|
+
/// Returns the photo URL of the current user, or null.
|
|
78
|
+
Future<String?> getCurrentUserPhotoUrl();
|
|
79
|
+
|
|
80
|
+
/// Returns all sign-in providers linked to the current account.
|
|
81
|
+
Future<List<String>> getLinkedProviders();
|
|
82
|
+
|
|
83
|
+
/// Sets or updates an email/password credential for the current user.
|
|
84
|
+
Future<void> setPassword(String password);
|
|
85
|
+
|
|
86
|
+
/// Social providers the current user can still link to their account.
|
|
87
|
+
Future<List<String>> linkableSocialProviders();
|
|
88
|
+
|
|
89
|
+
/// Links a social provider to the current account.
|
|
90
|
+
Future<void> linkSocialProvider(String provider);
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
class PhoneAlreadyLinkedException implements Exception {
|
|
@@ -99,6 +99,18 @@ abstract class AuthenticationRepository {
|
|
|
99
99
|
|
|
100
100
|
/// Returns the display name of the current user, or null.
|
|
101
101
|
Future<String?> getCurrentUserDisplayName();
|
|
102
|
+
|
|
103
|
+
/// Returns the photo URL of the current user, or null.
|
|
104
|
+
Future<String?> getCurrentUserPhotoUrl();
|
|
105
|
+
|
|
106
|
+
/// Returns the sign-in provider used ('google'|'apple'|'facebook'|'email'|'phone'), or null.
|
|
107
|
+
Future<List<String>> getLinkedProviders();
|
|
108
|
+
|
|
109
|
+
Future<void> setPassword(String password);
|
|
110
|
+
|
|
111
|
+
Future<List<String>> linkableSocialProviders();
|
|
112
|
+
|
|
113
|
+
Future<void> linkSocialProvider(String provider);
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
/// this is an example on how to create an authentication repository using firebase
|
|
@@ -403,4 +415,19 @@ class HttpAuthenticationRepository implements AuthenticationRepository {
|
|
|
403
415
|
|
|
404
416
|
@override
|
|
405
417
|
Future<String?> getCurrentUserDisplayName() => _authenticationApi.getCurrentUserDisplayName();
|
|
418
|
+
|
|
419
|
+
@override
|
|
420
|
+
Future<String?> getCurrentUserPhotoUrl() => _authenticationApi.getCurrentUserPhotoUrl();
|
|
421
|
+
|
|
422
|
+
@override
|
|
423
|
+
Future<List<String>> getLinkedProviders() => _authenticationApi.getLinkedProviders();
|
|
424
|
+
|
|
425
|
+
@override
|
|
426
|
+
Future<void> setPassword(String password) => _authenticationApi.setPassword(password);
|
|
427
|
+
|
|
428
|
+
@override
|
|
429
|
+
Future<List<String>> linkableSocialProviders() => _authenticationApi.linkableSocialProviders();
|
|
430
|
+
|
|
431
|
+
@override
|
|
432
|
+
Future<void> linkSocialProvider(String provider) => _authenticationApi.linkSocialProvider(provider);
|
|
406
433
|
}
|
|
@@ -36,6 +36,7 @@ sealed class SubscriptionEntity with _$SubscriptionEntity {
|
|
|
36
36
|
@JsonKey(name: 'creation_date') DateTime? creationDate,
|
|
37
37
|
@JsonKey(name: 'last_update_date') DateTime? lastUpdateDate,
|
|
38
38
|
@JsonKey(name: 'period_end_date') DateTime? periodEndDate,
|
|
39
|
+
@JsonKey(name: 'trial_end') DateTime? trialEnd,
|
|
39
40
|
@JsonKey(name: 'status') required SubscriptionStatus status,
|
|
40
41
|
@JsonKey(name: 'store', unknownEnumValue: SubscriptionStore.unknown)
|
|
41
42
|
SubscriptionStore? store,
|
|
@@ -710,37 +710,51 @@ async function checkBillingEnabled(projectId) {
|
|
|
710
710
|
* with only [localhost] would wipe firebaseapp.com / web.app. Idempotent: it's a
|
|
711
711
|
* no-op when both entries are already present.
|
|
712
712
|
*
|
|
713
|
+
* Right after a project's auth config is initialized, the Admin v2 config endpoint
|
|
714
|
+
* can briefly answer 403/404/409 before it settles (and a PATCH can 409 on a
|
|
715
|
+
* concurrent edit). Those are retried — re-reading each round so the merge stays
|
|
716
|
+
* correct; 400 and other client errors are treated as fatal. Best-effort: returns
|
|
717
|
+
* { ok: false } after exhausting retries instead of throwing.
|
|
718
|
+
*
|
|
719
|
+
* @param {string} projectId
|
|
720
|
+
* @param {string} token
|
|
721
|
+
* @param {{ maxRetries?: number, retryDelayMs?: number }} [opts]
|
|
713
722
|
* @returns {{ ok: boolean, added?: string[], error?: string }}
|
|
714
723
|
*/
|
|
715
|
-
async function ensureLocalhostAuthorizedDomains(projectId, token) {
|
|
724
|
+
async function ensureLocalhostAuthorizedDomains(projectId, token, { maxRetries = 4, retryDelayMs = 4000 } = {}) {
|
|
716
725
|
const base = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/config`;
|
|
717
726
|
const headers = {
|
|
718
727
|
Authorization: `Bearer ${token}`,
|
|
719
728
|
'Content-Type': 'application/json',
|
|
720
729
|
'X-Goog-User-Project': projectId,
|
|
721
730
|
};
|
|
722
|
-
// 1. Read the current authorized domains.
|
|
723
|
-
const getRes = await fetch(base, { headers });
|
|
724
|
-
if (!getRes.ok) {
|
|
725
|
-
const text = await getRes.text();
|
|
726
|
-
return { ok: false, error: `${getRes.status}: ${text}` };
|
|
727
|
-
}
|
|
728
|
-
const config = await getRes.json();
|
|
729
|
-
const current = Array.isArray(config.authorizedDomains) ? config.authorizedDomains : [];
|
|
730
731
|
const required = ['localhost', '127.0.0.1'];
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
732
|
+
const TRANSIENT = new Set([403, 404, 409, 429, 500, 503]);
|
|
733
|
+
let lastError = 'unknown';
|
|
734
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
735
|
+
// 1. Read the current authorized domains.
|
|
736
|
+
const getRes = await fetch(base, { headers });
|
|
737
|
+
if (!getRes.ok) {
|
|
738
|
+
lastError = `GET ${getRes.status}: ${await getRes.text()}`;
|
|
739
|
+
if (attempt < maxRetries && TRANSIENT.has(getRes.status)) { await sleep(retryDelayMs); continue; }
|
|
740
|
+
return { ok: false, error: lastError };
|
|
741
|
+
}
|
|
742
|
+
const config = await getRes.json();
|
|
743
|
+
const current = Array.isArray(config.authorizedDomains) ? config.authorizedDomains : [];
|
|
744
|
+
const missing = required.filter((d) => !current.includes(d));
|
|
745
|
+
if (missing.length === 0) return { ok: true, added: [] };
|
|
746
|
+
// 2. Merge and write back, keeping every domain that was already there.
|
|
747
|
+
const patchRes = await fetch(`${base}?updateMask=authorizedDomains`, {
|
|
748
|
+
method: 'PATCH',
|
|
749
|
+
headers,
|
|
750
|
+
body: JSON.stringify({ authorizedDomains: [...current, ...missing] }),
|
|
751
|
+
});
|
|
752
|
+
if (patchRes.ok) return { ok: true, added: missing };
|
|
753
|
+
lastError = `PATCH ${patchRes.status}: ${await patchRes.text()}`;
|
|
754
|
+
if (attempt < maxRetries && TRANSIENT.has(patchRes.status)) { await sleep(retryDelayMs); continue; }
|
|
755
|
+
return { ok: false, error: lastError };
|
|
742
756
|
}
|
|
743
|
-
return { ok:
|
|
757
|
+
return { ok: false, error: lastError };
|
|
744
758
|
}
|
|
745
759
|
|
|
746
760
|
/**
|
|
@@ -764,6 +778,168 @@ async function authorizeLocalhostForProject(projectId) {
|
|
|
764
778
|
return ensureLocalhostAuthorizedDomains(projectId, token);
|
|
765
779
|
}
|
|
766
780
|
|
|
781
|
+
/**
|
|
782
|
+
* Configure Apple Sign-In on Firebase for the WEB (and the OAuth code flow) by
|
|
783
|
+
* writing the Apple provider's codeFlowConfig (Service ID + Team ID + Key ID +
|
|
784
|
+
* `.p8`) via the Identity Toolkit Admin v2 API. Once stored, Firebase re-signs the
|
|
785
|
+
* short-lived client secret itself, so it never expires.
|
|
786
|
+
*
|
|
787
|
+
* Existing bundleIds (used by the native iOS flow) are preserved and the project's
|
|
788
|
+
* own bundleId is merged in, so configuring web never breaks native.
|
|
789
|
+
*
|
|
790
|
+
* @param {object} opts
|
|
791
|
+
* @param {string} opts.projectId
|
|
792
|
+
* @param {string} opts.serviceId - Apple Service ID (becomes the provider clientId)
|
|
793
|
+
* @param {string} opts.teamId
|
|
794
|
+
* @param {string} opts.keyId
|
|
795
|
+
* @param {string} opts.privateKey - PEM contents of the .p8
|
|
796
|
+
* @param {string} [opts.bundleId] - app bundle id to keep allowed for native sign-in
|
|
797
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
798
|
+
*/
|
|
799
|
+
async function configureFirebaseAppleWeb({ projectId, serviceId, teamId, keyId, privateKey, bundleId }) {
|
|
800
|
+
if (!serviceId || !teamId || !keyId || !privateKey) {
|
|
801
|
+
return { ok: false, error: 'serviceId, teamId, keyId and privateKey are required' };
|
|
802
|
+
}
|
|
803
|
+
let token;
|
|
804
|
+
try {
|
|
805
|
+
token = await getAccessToken();
|
|
806
|
+
} catch (_) {
|
|
807
|
+
return { ok: false, error: 'Could not get access token' };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const headers = {
|
|
811
|
+
Authorization: `Bearer ${token}`,
|
|
812
|
+
'Content-Type': 'application/json',
|
|
813
|
+
'X-Goog-User-Project': projectId,
|
|
814
|
+
};
|
|
815
|
+
const base = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/defaultSupportedIdpConfigs`;
|
|
816
|
+
|
|
817
|
+
// Read the current Apple config (if any) so we preserve the native bundleIds.
|
|
818
|
+
let existing = null;
|
|
819
|
+
try {
|
|
820
|
+
const getRes = await fetch(`${base}/apple.com`, { headers });
|
|
821
|
+
if (getRes.ok) existing = await getRes.json();
|
|
822
|
+
} catch (_) {
|
|
823
|
+
// No existing config (or transient) — we'll create it below.
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const bundleIds = Array.from(
|
|
827
|
+
new Set([...(existing?.appleSignInConfig?.bundleIds || []), bundleId].filter(Boolean)),
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
const body = {
|
|
831
|
+
name: `projects/${projectId}/defaultSupportedIdpConfigs/apple.com`,
|
|
832
|
+
enabled: true,
|
|
833
|
+
clientId: serviceId,
|
|
834
|
+
appleSignInConfig: {
|
|
835
|
+
codeFlowConfig: { teamId, keyId, privateKey },
|
|
836
|
+
...(bundleIds.length ? { bundleIds } : {}),
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
try {
|
|
841
|
+
if (existing) {
|
|
842
|
+
const res = await fetch(`${base}/apple.com?updateMask=enabled,clientId,appleSignInConfig`, {
|
|
843
|
+
method: 'PATCH',
|
|
844
|
+
headers,
|
|
845
|
+
body: JSON.stringify(body),
|
|
846
|
+
});
|
|
847
|
+
if (!res.ok) {
|
|
848
|
+
const text = await res.text();
|
|
849
|
+
return { ok: false, error: `PATCH failed (${res.status}): ${text.slice(0, 200)}` };
|
|
850
|
+
}
|
|
851
|
+
} else {
|
|
852
|
+
const res = await fetch(`${base}?idpId=apple.com`, {
|
|
853
|
+
method: 'POST',
|
|
854
|
+
headers,
|
|
855
|
+
body: JSON.stringify(body),
|
|
856
|
+
});
|
|
857
|
+
if (!res.ok) {
|
|
858
|
+
const text = await res.text();
|
|
859
|
+
// Raced with another writer — fall back to PATCH.
|
|
860
|
+
if (res.status === 409 || text.includes('ALREADY_EXISTS')) {
|
|
861
|
+
const patchRes = await fetch(`${base}/apple.com?updateMask=enabled,clientId,appleSignInConfig`, {
|
|
862
|
+
method: 'PATCH',
|
|
863
|
+
headers,
|
|
864
|
+
body: JSON.stringify(body),
|
|
865
|
+
});
|
|
866
|
+
if (!patchRes.ok) {
|
|
867
|
+
const t2 = await patchRes.text();
|
|
868
|
+
return { ok: false, error: `PATCH failed (${patchRes.status}): ${t2.slice(0, 200)}` };
|
|
869
|
+
}
|
|
870
|
+
} else {
|
|
871
|
+
return { ok: false, error: `POST failed (${res.status}): ${text.slice(0, 200)}` };
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
} catch (err) {
|
|
876
|
+
return { ok: false, error: `Network error: ${err.message}` };
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return { ok: true };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Enable the Facebook provider on Firebase via the Identity Toolkit Admin v2 API.
|
|
884
|
+
* Needs the Meta App ID (clientId) + App Secret (clientSecret). The native App ID /
|
|
885
|
+
* Client Token live in Info.plist / strings.xml and are written separately.
|
|
886
|
+
*
|
|
887
|
+
* @param {object} opts - { projectId, appId, appSecret }
|
|
888
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
889
|
+
*/
|
|
890
|
+
async function configureFirebaseFacebook({ projectId, appId, appSecret }) {
|
|
891
|
+
if (!appId || !appSecret) {
|
|
892
|
+
return { ok: false, error: 'appId and appSecret are required' };
|
|
893
|
+
}
|
|
894
|
+
let token;
|
|
895
|
+
try {
|
|
896
|
+
token = await getAccessToken();
|
|
897
|
+
} catch (_) {
|
|
898
|
+
return { ok: false, error: 'Could not get access token' };
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const headers = {
|
|
902
|
+
Authorization: `Bearer ${token}`,
|
|
903
|
+
'Content-Type': 'application/json',
|
|
904
|
+
'X-Goog-User-Project': projectId,
|
|
905
|
+
};
|
|
906
|
+
const base = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/defaultSupportedIdpConfigs`;
|
|
907
|
+
const body = {
|
|
908
|
+
name: `projects/${projectId}/defaultSupportedIdpConfigs/facebook.com`,
|
|
909
|
+
enabled: true,
|
|
910
|
+
clientId: appId,
|
|
911
|
+
clientSecret: appSecret,
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
let res = await fetch(`${base}?idpId=facebook.com`, {
|
|
916
|
+
method: 'POST',
|
|
917
|
+
headers,
|
|
918
|
+
body: JSON.stringify(body),
|
|
919
|
+
});
|
|
920
|
+
if (!res.ok) {
|
|
921
|
+
const text = await res.text();
|
|
922
|
+
if (res.status === 409 || text.includes('ALREADY_EXISTS')) {
|
|
923
|
+
res = await fetch(`${base}/facebook.com?updateMask=enabled,clientId,clientSecret`, {
|
|
924
|
+
method: 'PATCH',
|
|
925
|
+
headers,
|
|
926
|
+
body: JSON.stringify(body),
|
|
927
|
+
});
|
|
928
|
+
if (!res.ok) {
|
|
929
|
+
const t2 = await res.text();
|
|
930
|
+
return { ok: false, error: `PATCH failed (${res.status}): ${t2.slice(0, 200)}` };
|
|
931
|
+
}
|
|
932
|
+
} else {
|
|
933
|
+
return { ok: false, error: `POST failed (${res.status}): ${text.slice(0, 200)}` };
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
} catch (err) {
|
|
937
|
+
return { ok: false, error: `Network error: ${err.message}` };
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
return { ok: true };
|
|
941
|
+
}
|
|
942
|
+
|
|
767
943
|
/**
|
|
768
944
|
* Initialize Firebase Auth (Identity Platform) for a project. Brand-new projects
|
|
769
945
|
* have no auth config, so any Admin v2 operation (PATCH /config, or the Firebase
|
|
@@ -1299,6 +1475,8 @@ module.exports = {
|
|
|
1299
1475
|
ensureFirebaseAuthInitialized,
|
|
1300
1476
|
ensureLocalhostAuthorizedDomains,
|
|
1301
1477
|
authorizeLocalhostForProject,
|
|
1478
|
+
configureFirebaseAppleWeb,
|
|
1479
|
+
configureFirebaseFacebook,
|
|
1302
1480
|
listBillingAccounts,
|
|
1303
1481
|
listGcpOrganizations,
|
|
1304
1482
|
checkGcloudAuth,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"api/android/app/src/main/AndroidManifest.xml": "0579c57068ee50d43837ef9c7bee884b6232e92975301c8305ce389282f68291",
|
|
3
|
+
"api/ios/Runner/AppDelegate.swift": "9f1027a03ad1fef2dab45464fbd7e98364b6a9cb6d2a6abe770ee9c45fa19122",
|
|
4
|
+
"api/lib/core/data/api/storage_api.dart": "d77fecc6923ea9544b6ec20571dcab5c8f3d38be81ec618b0cf4f1b08be7bbae",
|
|
5
|
+
"api/lib/core/data/api/user_api.dart": "06f491ebf4548b87431e2d6983148f4082e120ec7d6a928108cba954d16d8c56",
|
|
6
|
+
"api/lib/core/data/entities/json_converters.dart": "92a5efd595a03a862fa03e77ab3ca3d556523d0c9e0daca6e296e35d24b760a8",
|
|
7
|
+
"api/lib/core/data/entities/user_entity.dart": "5c1e56e4be3ba2ed95cdcd276112a02b526f2a3fa7ee5bba08a3ec2b51153688",
|
|
8
|
+
"api/lib/environments.dart": "c41f0843a4d627b717579eec5607b7ff5c16369bdd4b2a82e081058a20ffb7a5",
|
|
9
|
+
"api/lib/features/ai_chat/api/ai_chat_api.dart": "749047d30390e8d624c5a17d421096b05651e28b34a42071a9b73efc71d0a5f7",
|
|
10
|
+
"api/lib/features/ai_chat/api/ai_chat_conversation_entity.dart": "1c4e4223e802d7fe8e93d9e7a09fcc6be81a41792d722e8b7e697837cde4737f",
|
|
11
|
+
"api/lib/features/ai_chat/api/ai_chat_message_entity.dart": "5aca4fdb5b1c38df04c9d377c6e6eeb6930ef85b2a32689fe6403e7cc7645373",
|
|
12
|
+
"api/lib/features/ai_chat/providers/ai_chat_notifier.dart": "f16ea3f3266e63a5591ceff9bc1900845f0b96e9bbbc5463ec6ab894f878eae3",
|
|
13
|
+
"api/lib/features/authentication/api/authentication_api_interface.dart": "ef9219237babd73e4b3ac148a24ebbbdf39ab98166d21f578c359074b528e5da",
|
|
14
|
+
"api/lib/features/authentication/api/authentication_api.dart": "8b460722bb3bee7eee015f34aaa39c1c97cb264df1024b2855060dac232c620a",
|
|
15
|
+
"api/lib/features/authentication/repositories/authentication_repository.dart": "8e98ca0d39df3aac1dab159e6bdfe8d2bbb8e43931459984eb4ecfb676698ca2",
|
|
16
|
+
"api/lib/features/feedbacks/api/entities/feature_request_entity.dart": "e72c683c7321bf1f3c19fc1c6d54c6964bd99970e87c9ff31d8918efa93f0090",
|
|
17
|
+
"api/lib/features/feedbacks/api/entities/feature_vote_entity.dart": "a13ccfc493d8f5277beffc8e28f77ec322ab0c0a56358c3669353206b7c6756b",
|
|
18
|
+
"api/lib/features/feedbacks/api/feature_request_api.dart": "a741395509b95e8cae135bc8bdcca1ba0d48785b1e1b201732219659d271346b",
|
|
19
|
+
"api/lib/features/feedbacks/api/feature_vote_api.dart": "89dcaeea29460a7f219c2ede3759707b45066272fe6837c29a40f3d79fade4e9",
|
|
20
|
+
"api/lib/features/notifications/api/device_api.dart": "0d6bccb6813cceec29c54220daac9d0cd35687c4fa9f10ffde07dad88b204ed3",
|
|
21
|
+
"api/lib/features/notifications/api/entities/device_entity.dart": "54fce6274c5fe60ba663d8693e0c9d88fd6cd6fcd9ec906e021621a2e51176b7",
|
|
22
|
+
"api/lib/features/notifications/api/entities/notifications_entity.dart": "f9ee62111a6f122657e105df317f1f5f803e3a525a96686c0a2728ff02cd8964",
|
|
23
|
+
"api/lib/features/notifications/api/notifications_api.dart": "30cdb1bc52c3249b2cda6b1c7eb7fb6a977db5744da5a9c2980784f41e75d9e7",
|
|
24
|
+
"api/lib/features/onboarding/api/entities/user_info_entity.dart": "cd5aabc68310025f3ab9676cab126e1f755431f9e8752f868ee9fcd6f5892ce2",
|
|
25
|
+
"api/lib/features/onboarding/api/user_infos_api.dart": "cf728bcef364259912ee1e87151c4f791faee0c0d49232c9af260bb5681bd429",
|
|
26
|
+
"api/lib/features/onboarding/models/user_info.dart": "946fd34a33b630a34a3b6248594cbca717f4fab9d23f669510145b9032bd1321",
|
|
27
|
+
"api/lib/features/onboarding/repositories/user_infos_repository.dart": "487a5177dcaed7f87e613b2b4f04329c4995274e6a52809d4917c037cb3c3c55",
|
|
28
|
+
"api/lib/features/settings/ui/components/admin/admin_users_api.dart": "0ffb232ffb193154c0366d6f7da0ca499b3e06fed4fd71af572b8dc53ecd844e",
|
|
29
|
+
"api/lib/features/settings/ui/widgets/avatar_utils.dart": "bb9126409bbbb245f2dec613bd096ac53c208a56bd55f3d2ab2599e43534904f",
|
|
30
|
+
"api/lib/features/subscriptions/api/entities/subscription_entity.dart": "20c1d75ed9d88acb96e94a592dcb4de0f63c792302ea07df53cebbdeb6d0cf7e",
|
|
31
|
+
"api/lib/features/subscriptions/api/stripe_backend_api.dart": "e370bd05211462f2fc94c69af1749eb5330f997e9ec5e73e5c4e119999bf666f",
|
|
32
|
+
"api/lib/features/subscriptions/api/subscription_api.dart": "c7484c9301d16245748025a3d420a89397e9e956b2211b7ed001c073ff2e4449",
|
|
33
|
+
"api/README.md": "1f30fc1ebf8fe02df6f3ee2f94c17f4bb4952abd5b125312d2c43eb6374eb354",
|
|
34
|
+
"supabase/android/app/src/main/AndroidManifest.xml": "0579c57068ee50d43837ef9c7bee884b6232e92975301c8305ce389282f68291",
|
|
35
|
+
"supabase/ios/Runner/AppDelegate.swift": "9f1027a03ad1fef2dab45464fbd7e98364b6a9cb6d2a6abe770ee9c45fa19122",
|
|
36
|
+
"supabase/lib/core/data/api/storage_api.dart": "d77fecc6923ea9544b6ec20571dcab5c8f3d38be81ec618b0cf4f1b08be7bbae",
|
|
37
|
+
"supabase/lib/core/data/api/user_api.dart": "06f491ebf4548b87431e2d6983148f4082e120ec7d6a928108cba954d16d8c56",
|
|
38
|
+
"supabase/lib/core/data/entities/json_converters.dart": "92a5efd595a03a862fa03e77ab3ca3d556523d0c9e0daca6e296e35d24b760a8",
|
|
39
|
+
"supabase/lib/core/data/entities/user_entity.dart": "5c1e56e4be3ba2ed95cdcd276112a02b526f2a3fa7ee5bba08a3ec2b51153688",
|
|
40
|
+
"supabase/lib/environments.dart": "c41f0843a4d627b717579eec5607b7ff5c16369bdd4b2a82e081058a20ffb7a5",
|
|
41
|
+
"supabase/lib/features/ai_chat/api/ai_chat_api.dart": "749047d30390e8d624c5a17d421096b05651e28b34a42071a9b73efc71d0a5f7",
|
|
42
|
+
"supabase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart": "1c4e4223e802d7fe8e93d9e7a09fcc6be81a41792d722e8b7e697837cde4737f",
|
|
43
|
+
"supabase/lib/features/ai_chat/api/ai_chat_message_entity.dart": "5aca4fdb5b1c38df04c9d377c6e6eeb6930ef85b2a32689fe6403e7cc7645373",
|
|
44
|
+
"supabase/lib/features/ai_chat/providers/ai_chat_notifier.dart": "f16ea3f3266e63a5591ceff9bc1900845f0b96e9bbbc5463ec6ab894f878eae3",
|
|
45
|
+
"supabase/lib/features/authentication/api/authentication_api.dart": "8b460722bb3bee7eee015f34aaa39c1c97cb264df1024b2855060dac232c620a",
|
|
46
|
+
"supabase/lib/features/authentication/repositories/authentication_repository.dart": "8e98ca0d39df3aac1dab159e6bdfe8d2bbb8e43931459984eb4ecfb676698ca2",
|
|
47
|
+
"supabase/lib/features/feedbacks/api/entities/feature_request_entity.dart": "e72c683c7321bf1f3c19fc1c6d54c6964bd99970e87c9ff31d8918efa93f0090",
|
|
48
|
+
"supabase/lib/features/feedbacks/api/entities/feature_vote_entity.dart": "a13ccfc493d8f5277beffc8e28f77ec322ab0c0a56358c3669353206b7c6756b",
|
|
49
|
+
"supabase/lib/features/feedbacks/api/feature_request_api.dart": "a741395509b95e8cae135bc8bdcca1ba0d48785b1e1b201732219659d271346b",
|
|
50
|
+
"supabase/lib/features/feedbacks/api/feature_vote_api.dart": "89dcaeea29460a7f219c2ede3759707b45066272fe6837c29a40f3d79fade4e9",
|
|
51
|
+
"supabase/lib/features/notifications/api/device_api.dart": "0d6bccb6813cceec29c54220daac9d0cd35687c4fa9f10ffde07dad88b204ed3",
|
|
52
|
+
"supabase/lib/features/notifications/api/entities/device_entity.dart": "54fce6274c5fe60ba663d8693e0c9d88fd6cd6fcd9ec906e021621a2e51176b7",
|
|
53
|
+
"supabase/lib/features/notifications/api/entities/notifications_entity.dart": "f9ee62111a6f122657e105df317f1f5f803e3a525a96686c0a2728ff02cd8964",
|
|
54
|
+
"supabase/lib/features/notifications/api/notifications_api.dart": "30cdb1bc52c3249b2cda6b1c7eb7fb6a977db5744da5a9c2980784f41e75d9e7",
|
|
55
|
+
"supabase/lib/features/onboarding/api/entities/user_info_entity.dart": "cd5aabc68310025f3ab9676cab126e1f755431f9e8752f868ee9fcd6f5892ce2",
|
|
56
|
+
"supabase/lib/features/onboarding/api/user_infos_api.dart": "cf728bcef364259912ee1e87151c4f791faee0c0d49232c9af260bb5681bd429",
|
|
57
|
+
"supabase/lib/features/onboarding/models/user_info.dart": "946fd34a33b630a34a3b6248594cbca717f4fab9d23f669510145b9032bd1321",
|
|
58
|
+
"supabase/lib/features/onboarding/repositories/user_infos_repository.dart": "487a5177dcaed7f87e613b2b4f04329c4995274e6a52809d4917c037cb3c3c55",
|
|
59
|
+
"supabase/lib/features/settings/ui/components/admin/admin_users_api.dart": "0ffb232ffb193154c0366d6f7da0ca499b3e06fed4fd71af572b8dc53ecd844e",
|
|
60
|
+
"supabase/lib/features/settings/ui/widgets/avatar_utils.dart": "bb9126409bbbb245f2dec613bd096ac53c208a56bd55f3d2ab2599e43534904f",
|
|
61
|
+
"supabase/lib/features/subscriptions/api/entities/subscription_entity.dart": "20c1d75ed9d88acb96e94a592dcb4de0f63c792302ea07df53cebbdeb6d0cf7e",
|
|
62
|
+
"supabase/lib/features/subscriptions/api/stripe_backend_api.dart": "e370bd05211462f2fc94c69af1749eb5330f997e9ec5e73e5c4e119999bf666f",
|
|
63
|
+
"supabase/lib/features/subscriptions/api/subscription_api.dart": "c7484c9301d16245748025a3d420a89397e9e956b2211b7ed001c073ff2e4449",
|
|
64
|
+
"supabase/lib/google_auth_options.dart": "9d3ab9be5928f3100b9b217864d916edd1223d186e60ed130c35628812cace66",
|
|
65
|
+
"supabase/README.md": "1f30fc1ebf8fe02df6f3ee2f94c17f4bb4952abd5b125312d2c43eb6374eb354"
|
|
66
|
+
}
|
|
@@ -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,
|
|
@@ -101,11 +101,13 @@ Deno.serve(async (req: Request) => {
|
|
|
101
101
|
const item = sub.items?.data?.[0];
|
|
102
102
|
const priceId = item?.price?.id ?? "";
|
|
103
103
|
const ms = periodEndMs(sub);
|
|
104
|
+
const trialMs = sub.trial_end ? sub.trial_end * 1000 : null;
|
|
104
105
|
const now = new Date().toISOString();
|
|
105
106
|
const payload = {
|
|
106
107
|
status: statusFromStripe(sub),
|
|
107
108
|
last_update_date: now,
|
|
108
109
|
period_end_date: ms ? new Date(ms).toISOString() : null,
|
|
110
|
+
trial_end: trialMs ? new Date(trialMs).toISOString() : null,
|
|
109
111
|
sku_id: priceId,
|
|
110
112
|
offer_id: priceId,
|
|
111
113
|
store: "STRIPE",
|
|
@@ -1,35 +1,36 @@
|
|
|
1
|
-
-- Send a welcome notification
|
|
1
|
+
-- Send a welcome notification ONCE per account when a device is registered.
|
|
2
2
|
--
|
|
3
3
|
-- Why on device registration and not on user creation?
|
|
4
|
-
-- At user creation time no device token exists yet. By firing on
|
|
5
|
-
--
|
|
6
|
-
--
|
|
4
|
+
-- At user creation time no device token exists yet. By firing on INSERT into
|
|
5
|
+
-- `devices` we guarantee the locale is set and the notification can be localised
|
|
6
|
+
-- accurately.
|
|
7
7
|
--
|
|
8
|
-
--
|
|
9
|
-
--
|
|
8
|
+
-- Why a `welcome_sent` flag and not a device count?
|
|
9
|
+
-- Logout removes the device row and login re-creates it, so a device count
|
|
10
|
+
-- ("is this the first device?") sees a brand-new first device on every
|
|
11
|
+
-- re-login and re-sent the welcome each time. A one-shot flag on the user,
|
|
12
|
+
-- claimed atomically, sends it exactly once for the life of the account.
|
|
10
13
|
--
|
|
11
|
-
--
|
|
12
|
-
--
|
|
14
|
+
-- notify_user = false: the user is already inside the app at registration time,
|
|
15
|
+
-- so no push is needed — the notification is saved to the DB only.
|
|
16
|
+
--
|
|
17
|
+
-- Anonymous users (no email in public.users) are skipped until they have one.
|
|
18
|
+
|
|
19
|
+
-- One-time welcome guard (idempotent so re-running the migration is safe).
|
|
20
|
+
ALTER TABLE public.users
|
|
21
|
+
ADD COLUMN IF NOT EXISTS welcome_sent boolean NOT NULL DEFAULT false;
|
|
13
22
|
|
|
14
23
|
CREATE OR REPLACE FUNCTION public.trigger_welcome_notification()
|
|
15
24
|
RETURNS TRIGGER AS $$
|
|
16
25
|
DECLARE
|
|
17
|
-
v_email
|
|
18
|
-
v_locale
|
|
19
|
-
|
|
20
|
-
v_title
|
|
21
|
-
v_body
|
|
26
|
+
v_email text;
|
|
27
|
+
v_locale text;
|
|
28
|
+
v_claimed int;
|
|
29
|
+
v_title text;
|
|
30
|
+
v_body text;
|
|
22
31
|
BEGIN
|
|
23
|
-
--
|
|
24
|
-
|
|
25
|
-
FROM public.devices
|
|
26
|
-
WHERE user_id = NEW.user_id AND id != NEW.id;
|
|
27
|
-
|
|
28
|
-
IF v_count > 0 THEN
|
|
29
|
-
RETURN NEW;
|
|
30
|
-
END IF;
|
|
31
|
-
|
|
32
|
-
-- Skip anonymous users (no email set yet).
|
|
32
|
+
-- Skip anonymous users (no email yet); they get welcomed once they have one,
|
|
33
|
+
-- WITHOUT consuming the one-time claim below.
|
|
33
34
|
SELECT email, locale INTO v_email, v_locale
|
|
34
35
|
FROM public.users
|
|
35
36
|
WHERE id = NEW.user_id;
|
|
@@ -38,6 +39,18 @@ BEGIN
|
|
|
38
39
|
RETURN NEW;
|
|
39
40
|
END IF;
|
|
40
41
|
|
|
42
|
+
-- Atomically claim the one-time welcome: flip welcome_sent to true only if it
|
|
43
|
+
-- isn't already. ROW_COUNT is 1 for the very first claim, 0 for any later
|
|
44
|
+
-- device registration — including one re-created after a logout/login cycle.
|
|
45
|
+
UPDATE public.users
|
|
46
|
+
SET welcome_sent = true
|
|
47
|
+
WHERE id = NEW.user_id AND welcome_sent = false;
|
|
48
|
+
|
|
49
|
+
GET DIAGNOSTICS v_claimed = ROW_COUNT;
|
|
50
|
+
IF v_claimed = 0 THEN
|
|
51
|
+
RETURN NEW;
|
|
52
|
+
END IF;
|
|
53
|
+
|
|
41
54
|
-- Localised welcome message (falls back to English).
|
|
42
55
|
IF v_locale = 'pt' THEN
|
|
43
56
|
v_title := 'Bem-vindo!';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- Adds trial_end to subscriptions so trial periods are persisted and the app
|
|
2
|
+
-- can detect an active trial. Mirrors the Firebase backend, where the Stripe
|
|
3
|
+
-- webhook writes trial_end (RevenueCat trials are read from the SDK, not the DB).
|
|
4
|
+
-- Idempotent: safe to run on a fresh project or an already-deployed database.
|
|
5
|
+
ALTER TABLE public.subscriptions
|
|
6
|
+
ADD COLUMN IF NOT EXISTS trial_end TIMESTAMPTZ;
|