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.
Files changed (169) hide show
  1. package/README.md +1 -1
  2. package/bin/kasy.js +66 -2
  3. package/docs/cli-reference.md +7 -7
  4. package/lib/commands/apple-web.js +222 -0
  5. package/lib/commands/configure.js +3 -91
  6. package/lib/commands/doctor.js +20 -0
  7. package/lib/commands/facebook.js +189 -0
  8. package/lib/commands/new.js +61 -11
  9. package/lib/commands/release-version.js +234 -0
  10. package/lib/commands/update.js +27 -0
  11. package/lib/scaffold/CHANGELOG.json +27 -0
  12. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
  13. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
  14. package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
  15. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  16. package/lib/scaffold/backends/firebase/setup-from-scratch.js +199 -21
  17. package/lib/scaffold/backends/patch-base-hashes.json +66 -0
  18. package/lib/scaffold/backends/supabase/deploy.js +92 -0
  19. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
  20. package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
  21. package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
  22. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +92 -3
  23. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
  24. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  25. package/lib/scaffold/generate.js +53 -4
  26. package/lib/scaffold/shared/generator-utils.js +18 -6
  27. package/lib/utils/apple-web.js +147 -0
  28. package/lib/utils/facebook.js +162 -0
  29. package/lib/utils/i18n/messages-en.js +85 -0
  30. package/lib/utils/i18n/messages-es.js +85 -0
  31. package/lib/utils/i18n/messages-pt.js +85 -0
  32. package/package.json +5 -2
  33. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
  34. package/templates/firebase/AGENTS.md +170 -0
  35. package/templates/firebase/CLAUDE.md +16 -0
  36. package/templates/firebase/DESIGN_SYSTEM.md +269 -0
  37. package/templates/firebase/docs/auth-setup.en.md +4 -2
  38. package/templates/firebase/docs/auth-setup.es.md +4 -2
  39. package/templates/firebase/docs/auth-setup.pt.md +4 -2
  40. package/templates/firebase/firebase.json +56 -1
  41. package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
  42. package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
  43. package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
  44. package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
  45. package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
  46. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
  47. package/templates/firebase/lib/components/components.dart +1 -0
  48. package/templates/firebase/lib/components/kasy_alert.dart +0 -1
  49. package/templates/firebase/lib/components/kasy_app_bar.dart +35 -17
  50. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
  51. package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
  52. package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
  53. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
  54. package/templates/firebase/lib/components/kasy_screen.dart +114 -0
  55. package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
  56. package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
  57. package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
  58. package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
  59. package/templates/firebase/lib/components/kasy_toast.dart +108 -73
  60. package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
  61. package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
  62. package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
  63. package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
  64. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
  65. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
  66. package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
  67. package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
  68. package/templates/firebase/lib/core/config/features.dart +5 -0
  69. package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
  70. package/templates/firebase/lib/core/guards/guard.dart +16 -2
  71. package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
  72. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
  73. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +48 -124
  74. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
  75. package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
  76. package/templates/firebase/lib/core/states/logout_action.dart +5 -1
  77. package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
  78. package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
  79. package/templates/firebase/lib/core/theme/texts.dart +90 -57
  80. package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
  81. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
  82. package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
  83. package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
  84. package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
  85. package/templates/firebase/lib/core/web_screen_width.dart +15 -0
  86. package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
  87. package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
  88. package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
  89. package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
  90. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
  91. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -8
  92. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
  93. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
  94. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
  95. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
  96. package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
  97. package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
  98. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +266 -0
  99. package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
  100. package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
  101. package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
  102. package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
  103. package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
  104. package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
  105. package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
  106. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
  107. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
  108. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +80 -15
  109. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +20 -14
  110. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
  111. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
  112. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
  113. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -2
  114. package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
  115. package/templates/firebase/lib/features/home/home_components_page.dart +8 -1
  116. package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
  117. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +186 -56
  118. package/templates/firebase/lib/features/home/home_page.dart +4 -0
  119. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +169 -208
  120. package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
  121. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
  122. package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
  123. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
  124. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -4
  125. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +84 -128
  126. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
  127. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
  128. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
  129. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
  130. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
  131. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +2 -1
  132. package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
  133. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +58 -21
  134. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
  135. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
  136. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
  137. package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
  138. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
  139. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
  140. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
  141. package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
  142. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
  143. package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
  144. package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
  145. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
  146. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
  147. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
  148. package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
  149. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
  150. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
  151. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
  152. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
  153. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
  154. package/templates/firebase/lib/i18n/en.i18n.json +54 -7
  155. package/templates/firebase/lib/i18n/es.i18n.json +54 -7
  156. package/templates/firebase/lib/i18n/pt.i18n.json +54 -7
  157. package/templates/firebase/lib/main.dart +11 -2
  158. package/templates/firebase/lib/router.dart +94 -13
  159. package/templates/firebase/pubspec.yaml +1 -1
  160. package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
  161. package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
  162. package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
  163. package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
  164. package/templates/firebase/tool/design_check.dart +152 -0
  165. package/templates/firebase/web/index.html +162 -14
  166. package/templates/firebase/assets/images/review.png +0 -0
  167. package/templates/firebase/assets/images/update.png +0 -0
  168. package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
  169. package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
@@ -1,3 +1,4 @@
1
+ import 'dart:async';
1
2
  import 'dart:convert';
2
3
  import 'package:crypto/crypto.dart';
3
4
  import 'package:firebase_auth/firebase_auth.dart' as fb_auth;
@@ -11,6 +12,8 @@ import 'package:kasy_kit/core/data/api/base_api_exceptions.dart';
11
12
  import 'package:kasy_kit/core/data/entities/user_entity.dart';
12
13
  import 'package:kasy_kit/environments.dart';
13
14
  import 'package:kasy_kit/google_auth_options.dart';
15
+ import 'package:kasy_kit/features/authentication/api/auth_web_support.dart'
16
+ if (dart.library.js_interop) 'package:kasy_kit/features/authentication/api/auth_web_support_web.dart';
14
17
  import 'package:kasy_kit/features/authentication/api/authentication_api_interface.dart';
15
18
  import 'package:kasy_kit/features/authentication/repositories/exceptions/authentication_exceptions.dart';
16
19
  import 'package:sign_in_with_apple/sign_in_with_apple.dart';
@@ -34,7 +37,41 @@ class SupabaseAuthenticationApi implements AuthenticationApi {
34
37
  });
35
38
 
36
39
  @override
37
- Future<void> init() async {}
40
+ Future<void> init() async {
41
+ if (!kIsWeb) return;
42
+ // Complete a pending mobile-web Google redirect sign-in. Firebase handled the
43
+ // full-page redirect (we reuse its web OAuth client to mint the Google ID
44
+ // token — see [signinWithGoogle]), so on return we exchange that token for a
45
+ // Supabase session. No-op when no redirect is pending or a session already
46
+ // exists.
47
+ try {
48
+ final result = await fb_auth.FirebaseAuth.instance.getRedirectResult();
49
+ final idToken = (result.credential as fb_auth.OAuthCredential?)?.idToken;
50
+ if (idToken != null && client.auth.currentUser == null) {
51
+ await client.auth.signInWithIdToken(
52
+ provider: OAuthProvider.google,
53
+ idToken: idToken,
54
+ );
55
+ }
56
+ } catch (e) {
57
+ _logger.w('Google redirect sign-in completion failed: $e');
58
+ }
59
+ }
60
+
61
+ /// Mobile-web Google sign-in: start a full-page Firebase redirect instead of a
62
+ /// popup. Mobile browsers handle popups unreliably (the result is often lost
63
+ /// when the browser reclaims the backgrounded opener tab), leaving the app
64
+ /// stuck "signing in". The Supabase session is established at startup by [init]
65
+ /// via getRedirectResult. The page is navigating away as soon as
66
+ /// [signInWithRedirect] resolves, so this future intentionally never completes
67
+ /// — the caller stays in its loading state until the browser leaves.
68
+ /// See [isMobileWebBrowser].
69
+ Future<Credentials> _googleSignInWithRedirectWeb() async {
70
+ await fb_auth.FirebaseAuth.instance.signInWithRedirect(
71
+ fb_auth.GoogleAuthProvider(),
72
+ );
73
+ return Completer<Credentials>().future;
74
+ }
38
75
 
39
76
  @override
40
77
  Future<void> recoverPassword(String email) {
@@ -192,6 +229,11 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
192
229
 
193
230
  @override
194
231
  Future<Credentials> signinWithFacebook() async {
232
+ // Facebook on web for Supabase is not wired yet (roadmap); the button is hidden
233
+ // on web, so this is a defensive guard.
234
+ if (kIsWeb) {
235
+ throw ApiError(code: 501, message: 'Facebook sign-in on web is not supported on Supabase.');
236
+ }
195
237
  final loginResult = await FacebookAuth.instance.login(
196
238
  permissions: ['email', 'public_profile'],
197
239
  );
@@ -220,6 +262,7 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
220
262
  @override
221
263
  Future<Credentials> signinWithGoogle() async {
222
264
  if (kIsWeb) {
265
+ if (isMobileWebBrowser()) return _googleSignInWithRedirectWeb();
223
266
  // google_sign_in v7 can't do imperative auth on web. Get the Google ID token
224
267
  // via Firebase's popup (zero manual config — reuses the Firebase web OAuth
225
268
  // client + authorized domains, which the kit already sets up) and sign into
@@ -363,9 +406,21 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
363
406
  @override
364
407
  Future<Credentials> signupFromAnonymousWithGoogle() async {
365
408
  if (kIsWeb) {
366
- // Web: get the Google ID token via Firebase's popup (see signinWithGoogle) and
367
- // link it to the current anonymous Supabase user.
409
+ if (isMobileWebBrowser()) return _googleSignInWithRedirectWeb();
410
+ // Web: get the Google ID token via Firebase's popup (see signinWithGoogle).
368
411
  final idToken = await _googleIdTokenFromWebPopup();
412
+ // On web the app runs in authRequired mode, so there is usually no anonymous
413
+ // Supabase session to link to (the user state is just a local placeholder).
414
+ // Linking without a session sends the anon key, which has no `sub`, and Supabase
415
+ // rejects it with "missing sub claim". When there's no session, sign in normally
416
+ // — it creates the account from the Google id_token. Mirrors the Firebase backend.
417
+ if (client.auth.currentUser == null) {
418
+ final res = await client.auth.signInWithIdToken(
419
+ provider: OAuthProvider.google,
420
+ idToken: idToken,
421
+ );
422
+ return Credentials(id: res.user!.id, token: res.session?.accessToken ?? '');
423
+ }
369
424
  try {
370
425
  final response = await client.auth.linkIdentityWithIdToken(
371
426
  provider: OAuthProvider.google,
@@ -439,6 +494,11 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
439
494
 
440
495
  @override
441
496
  Future<Credentials> signupFromAnonymousWithFacebook() async {
497
+ // Facebook on web for Supabase is not wired yet (roadmap); the button is hidden
498
+ // on web, so this is a defensive guard.
499
+ if (kIsWeb) {
500
+ throw ApiError(code: 501, message: 'Facebook sign-in on web is not supported on Supabase.');
501
+ }
442
502
  final loginResult = await FacebookAuth.instance.login(
443
503
  permissions: ['email', 'public_profile'],
444
504
  );
@@ -595,6 +655,35 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
595
655
  Future<String?> getCurrentUserDisplayName() async =>
596
656
  client.auth.currentUser?.userMetadata?['full_name'] as String?;
597
657
 
658
+ @override
659
+ Future<String?> getCurrentUserPhotoUrl() async {
660
+ final meta = client.auth.currentUser?.userMetadata;
661
+ return (meta?['avatar_url'] ?? meta?['picture']) as String?;
662
+ }
663
+
664
+ @override
665
+ Future<List<String>> getLinkedProviders() async {
666
+ // Supabase exposes the linked identities in app_metadata['providers'].
667
+ final raw = client.auth.currentUser?.appMetadata['providers'];
668
+ if (raw is! List) return const [];
669
+ return raw.map((p) => p.toString()).toList();
670
+ }
671
+
672
+ @override
673
+ Future<void> setPassword(String password) async {
674
+ // Supabase keeps the same user; sets/updates the password so a social-only
675
+ // account can also sign in with email + password.
676
+ await client.auth.updateUser(UserAttributes(password: password));
677
+ }
678
+
679
+ // Supabase auto-links identities with the same (confirmed) email on sign-in,
680
+ // so there is no manual-link step: nothing to offer and nothing to do.
681
+ @override
682
+ Future<List<String>> linkableSocialProviders() async => const [];
683
+
684
+ @override
685
+ Future<void> linkSocialProvider(String provider) => Future.value();
686
+
598
687
  String _normalizePhoneNumber(String phoneNumber) {
599
688
  String normalized = phoneNumber.replaceAll(RegExp(r'\D'), '');
600
689
  if (!normalized.startsWith('+')) {
@@ -68,6 +68,22 @@ abstract class AuthenticationRepository {
68
68
  /// Returns the display name of the authenticated user.
69
69
  Future<String?> getCurrentUserDisplayName();
70
70
 
71
+ /// Returns the photo URL of the current user (e.g. Google picture), or null.
72
+ Future<String?> getCurrentUserPhotoUrl();
73
+
74
+ /// Returns all sign-in providers linked to the current account.
75
+ Future<List<String>> getLinkedProviders();
76
+
77
+ /// Sets or updates an email/password credential for the current user, so a
78
+ /// social-only account can also sign in with email + password.
79
+ Future<void> setPassword(String password);
80
+
81
+ /// Social providers the current user can still link to their account.
82
+ Future<List<String>> linkableSocialProviders();
83
+
84
+ /// Links a social provider to the current account.
85
+ Future<void> linkSocialProvider(String provider);
86
+
71
87
  /// Signin with Google Play Games account on Android
72
88
  Future<void> signinWithGooglePlayGames();
73
89
 
@@ -268,6 +284,26 @@ class HttpAuthenticationRepository implements AuthenticationRepository {
268
284
  Future<String?> getCurrentUserDisplayName() =>
269
285
  _authenticationApi.getCurrentUserDisplayName();
270
286
 
287
+ @override
288
+ Future<String?> getCurrentUserPhotoUrl() =>
289
+ _authenticationApi.getCurrentUserPhotoUrl();
290
+
291
+ @override
292
+ Future<List<String>> getLinkedProviders() =>
293
+ _authenticationApi.getLinkedProviders();
294
+
295
+ @override
296
+ Future<void> setPassword(String password) =>
297
+ _authenticationApi.setPassword(password);
298
+
299
+ @override
300
+ Future<List<String>> linkableSocialProviders() =>
301
+ _authenticationApi.linkableSocialProviders();
302
+
303
+ @override
304
+ Future<void> linkSocialProvider(String provider) =>
305
+ _authenticationApi.linkSocialProvider(provider);
306
+
271
307
  @override
272
308
  Future<void> signinWithGooglePlayGames() async {
273
309
  try {
@@ -37,6 +37,7 @@ sealed class SubscriptionEntity with _$SubscriptionEntity {
37
37
  @JsonKey(name: 'creation_date') DateTime? creationDate,
38
38
  @JsonKey(name: 'last_update_date') DateTime? lastUpdateDate,
39
39
  @JsonKey(name: 'period_end_date') DateTime? periodEndDate,
40
+ @JsonKey(name: 'trial_end') DateTime? trialEnd,
40
41
  @JsonKey(name: 'status') required SubscriptionStatus status,
41
42
  @JsonKey(name: 'store', unknownEnumValue: SubscriptionStore.unknown)
42
43
  SubscriptionStore? store,
@@ -86,11 +86,50 @@ const { FIREBASE_SOURCE_DIR } = require('./shared/backend-config');
86
86
  * @param {object} [options.moduleAnswers={}]
87
87
  * @param {Function} [options.onProgress] - Callback to update the spinner in the CLI.
88
88
  * @param {object} [options.deploy] - Used by the Firebase postBuild hook.
89
+ * @param {boolean} [options.offline=false] - Validation mode: never touch the
90
+ * network or the live Firebase project. Skips flutterfire configure, the Google
91
+ * auth/iOS/SW patches it feeds, and the Firestore rules deploy; writes a
92
+ * compilable stub for firebase_options_dev.dart so `flutter analyze` still works.
89
93
  * @param {object} [hooks={}]
90
94
  * @param {Function|null} [hooks.applyBackendSetup]
91
95
  * @param {Function|null} [hooks.postBuild]
92
96
  * @returns {Promise<{steps: object[], packageName: string, appName: string, bundleId: string, firebaseProjectId: string}>}
93
97
  */
98
+
99
+ /**
100
+ * Writes a compilable placeholder for lib/firebase_options_dev.dart, the file
101
+ * `flutterfire configure` normally generates from the real Firebase project.
102
+ *
103
+ * Used only by offline validation (`npm run validate:backends`): every backend's
104
+ * main.dart imports this file (Firebase powers FCM/remote config everywhere), so
105
+ * without it `flutter analyze` fails with "uri doesn't exist". The stub exposes
106
+ * the single member main.dart uses — DefaultFirebaseOptions.currentPlatform — with
107
+ * dummy values, so the analyzer type-checks the whole project without any network.
108
+ */
109
+ async function writeFirebaseOptionsDevStub(targetDir) {
110
+ const stub = `// Offline validation stub written by \`npm run validate:backends\`.
111
+ // NOT shipped to clients: \`kasy new\` runs \`flutterfire configure\`, which
112
+ // generates the real file from their Firebase project. Present only so
113
+ // \`flutter analyze\` can type-check without touching the network.
114
+ // ignore_for_file: type=lint
115
+ import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
116
+
117
+ class DefaultFirebaseOptions {
118
+ static FirebaseOptions get currentPlatform => const FirebaseOptions(
119
+ apiKey: 'offline-validation-stub',
120
+ appId: 'offline-validation-stub',
121
+ messagingSenderId: 'offline-validation-stub',
122
+ projectId: 'offline-validation-stub',
123
+ );
124
+ }
125
+ `;
126
+ await fs.outputFile(
127
+ path.join(targetDir, 'lib', 'firebase_options_dev.dart'),
128
+ stub,
129
+ 'utf8',
130
+ );
131
+ }
132
+
94
133
  async function generateProject(targetDir, backend, options, hooks = {}) {
95
134
  const {
96
135
  appName,
@@ -102,6 +141,7 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
102
141
  includeWeb = true,
103
142
  language = 'en',
104
143
  deferGoogleAuthPatches = false,
144
+ offline = false,
105
145
  } = options;
106
146
 
107
147
  const { applyBackendSetup = null, postBuild = null } = hooks;
@@ -361,9 +401,18 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
361
401
  steps.push({ name: 'build-runner', skipped: true });
362
402
  }
363
403
 
364
- onProgress('flutterfire');
365
- const ffResult = await flutterfireConfigure(targetDir, firebaseProjectId, { includeWeb });
366
- steps.push({ name: 'flutterfire', ok: ffResult.ok, detail: ffResult.ok ? null : ffResult.error });
404
+ // Offline validation never touches the network or the live Firebase project, so
405
+ // skip flutterfire and write a compilable stub for the file it would generate.
406
+ // ffResult.ok stays false the flutterfire-dependent patches below are skipped.
407
+ let ffResult = { ok: false };
408
+ if (offline) {
409
+ await writeFirebaseOptionsDevStub(targetDir);
410
+ steps.push({ name: 'flutterfire', skipped: true, detail: 'offline (stub written)' });
411
+ } else {
412
+ onProgress('flutterfire');
413
+ ffResult = await flutterfireConfigure(targetDir, firebaseProjectId, { includeWeb });
414
+ steps.push({ name: 'flutterfire', ok: ffResult.ok, detail: ffResult.ok ? null : ffResult.error });
415
+ }
367
416
 
368
417
  // After flutterfire: patch Android, iOS and Web config files.
369
418
  if (ffResult.ok) {
@@ -421,7 +470,7 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
421
470
  // ONLY for FCM push — there is no Firestore there, so deploying firestore:rules
422
471
  // is incoherent (it targets a database that doesn't exist) and just hangs for
423
472
  // minutes. The data layer for those backends is Supabase/the REST API.
424
- if (backend === 'firebase' && firebaseProjectId) {
473
+ if (!offline && backend === 'firebase' && firebaseProjectId) {
425
474
  onProgress('firestore-rules');
426
475
  const rulesResult = await deployFirestoreRules(targetDir, firebaseProjectId);
427
476
  steps.push({ name: 'firestore-rules', ok: rulesResult.ok, detail: rulesResult.ok ? null : rulesResult.error });
@@ -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 only works on Firebase (signInWithPopup). Supabase/API
628
- // throw on web (no Service ID + token exchange), so they must ship this false
629
- // and hide the Apple button on web. Native always shows it.
630
- const withAppleWebSignin = backend === 'firebase';
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: Firebase supports it (signInWithPopup); Supabase/API throw
648
- // on web, so they ship false. Native always shows the Apple button.
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
+ };
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Facebook Login helpers, shared by `kasy facebook` and `kasy configure`.
3
+ *
4
+ * Facebook needs values in two places:
5
+ * - Client side (the app): App ID + Client Token in iOS Info.plist and Android
6
+ * strings.xml (build-time). Same for every backend.
7
+ * - Backend provider (server side): App ID + App Secret on Firebase (Identity
8
+ * Toolkit) or Supabase (Management API). Configured by the backend writers.
9
+ *
10
+ * The Meta-side work (create the app, get App ID / Client Token / App Secret, set
11
+ * OAuth redirect URIs and JS domains) has no automation API — it stays manual and
12
+ * is guided by `kasy facebook`. This module owns the native-file read/write and the
13
+ * ~/.kasy/facebook.json credential cache so future projects reuse the values.
14
+ */
15
+
16
+ const os = require('node:os');
17
+ const path = require('node:path');
18
+ const fs = require('fs-extra');
19
+
20
+ const CONFIG_DIR = path.join(os.homedir(), '.kasy');
21
+ const FACEBOOK_CONFIG_PATH = path.join(CONFIG_DIR, 'facebook.json');
22
+
23
+ // ── Native build-time files (Info.plist + strings.xml) ──────────────────────
24
+
25
+ function readFacebookCurrent(content, kind) {
26
+ if (kind === 'plist') {
27
+ const appId = content.match(/<key>FacebookAppID<\/key>\s*<string>([^<]*)<\/string>/);
28
+ const token = content.match(/<key>FacebookClientToken<\/key>\s*<string>([^<]*)<\/string>/);
29
+ return { appId: (appId?.[1] || '').trim(), token: (token?.[1] || '').trim() };
30
+ }
31
+ // strings.xml
32
+ const appId = content.match(/<string name="facebook_app_id"[^>]*>([^<]*)<\/string>/);
33
+ const token = content.match(/<string name="facebook_client_token"[^>]*>([^<]*)<\/string>/);
34
+ return { appId: (appId?.[1] || '').trim(), token: (token?.[1] || '').trim() };
35
+ }
36
+
37
+ function isFacebookPlaceholder(value) {
38
+ return !value || /^0+$/.test(value);
39
+ }
40
+
41
+ async function readFacebookState(projectDir) {
42
+ const plistPath = path.join(projectDir, 'ios', 'Runner', 'Info.plist');
43
+ const stringsPath = path.join(projectDir, 'android', 'app', 'src', 'main', 'res', 'values', 'strings.xml');
44
+ const state = { appId: '', token: '', plistExists: false, stringsExists: false };
45
+ if (await fs.pathExists(plistPath)) {
46
+ state.plistExists = true;
47
+ const plistRead = readFacebookCurrent(await fs.readFile(plistPath, 'utf8'), 'plist');
48
+ if (!isFacebookPlaceholder(plistRead.appId)) state.appId = plistRead.appId;
49
+ if (!isFacebookPlaceholder(plistRead.token)) state.token = plistRead.token;
50
+ }
51
+ if (await fs.pathExists(stringsPath)) {
52
+ state.stringsExists = true;
53
+ const stringsRead = readFacebookCurrent(await fs.readFile(stringsPath, 'utf8'), 'strings');
54
+ // Prefer plist value when both exist; only fall back if plist was placeholder.
55
+ if (!state.appId && !isFacebookPlaceholder(stringsRead.appId)) state.appId = stringsRead.appId;
56
+ if (!state.token && !isFacebookPlaceholder(stringsRead.token)) state.token = stringsRead.token;
57
+ }
58
+ return state;
59
+ }
60
+
61
+ async function writeFacebookCredentials(projectDir, appId, clientToken) {
62
+ const plistPath = path.join(projectDir, 'ios', 'Runner', 'Info.plist');
63
+ const stringsPath = path.join(projectDir, 'android', 'app', 'src', 'main', 'res', 'values', 'strings.xml');
64
+ const results = { plist: 'skipped', strings: 'skipped' };
65
+
66
+ if (appId && (await fs.pathExists(plistPath))) {
67
+ let plist = await fs.readFile(plistPath, 'utf8');
68
+ plist = plist.replace(
69
+ /(<key>FacebookAppID<\/key>\s*<string>)[^<]*(<\/string>)/,
70
+ `$1${appId}$2`,
71
+ );
72
+ if (clientToken) {
73
+ plist = plist.replace(
74
+ /(<key>FacebookClientToken<\/key>\s*<string>)[^<]*(<\/string>)/,
75
+ `$1${clientToken}$2`,
76
+ );
77
+ }
78
+ // CFBundleURLSchemes — replace `fb<zeros>` with `fb<appId>` so deep links work.
79
+ plist = plist.replace(/<string>fb0+<\/string>/, `<string>fb${appId}</string>`);
80
+ await fs.outputFile(plistPath, plist, 'utf8');
81
+ results.plist = 'ok';
82
+ } else if (appId) {
83
+ results.plist = 'missing_file';
84
+ }
85
+
86
+ if (appId && (await fs.pathExists(stringsPath))) {
87
+ let xml = await fs.readFile(stringsPath, 'utf8');
88
+ xml = xml.replace(
89
+ /(<string name="facebook_app_id"[^>]*>)[^<]*(<\/string>)/,
90
+ `$1${appId}$2`,
91
+ );
92
+ if (clientToken) {
93
+ xml = xml.replace(
94
+ /(<string name="facebook_client_token"[^>]*>)[^<]*(<\/string>)/,
95
+ `$1${clientToken}$2`,
96
+ );
97
+ }
98
+ // fb_login_protocol_scheme — set if present.
99
+ xml = xml.replace(
100
+ /(<string name="fb_login_protocol_scheme"[^>]*>)[^<]*(<\/string>)/,
101
+ `$1fb${appId}$2`,
102
+ );
103
+ await fs.outputFile(stringsPath, xml, 'utf8');
104
+ results.strings = 'ok';
105
+ } else if (appId) {
106
+ results.strings = 'missing_file';
107
+ }
108
+
109
+ return results;
110
+ }
111
+
112
+ // ── Credential cache (~/.kasy/facebook.json) ────────────────────────────────
113
+
114
+ /** Load cached Facebook credentials. Returns null if absent or without an App ID. */
115
+ async function loadFacebookCreds() {
116
+ try {
117
+ if (!(await fs.pathExists(FACEBOOK_CONFIG_PATH))) return null;
118
+ const data = await fs.readJson(FACEBOOK_CONFIG_PATH);
119
+ if (!data || !data.appId) return null;
120
+ return data;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ /** Persist Facebook credentials (App ID + Client Token + App Secret) with 0600. */
127
+ async function saveFacebookCreds({ appId, clientToken, appSecret }) {
128
+ await fs.ensureDir(CONFIG_DIR);
129
+ await fs.writeJson(
130
+ FACEBOOK_CONFIG_PATH,
131
+ { appId, clientToken: clientToken || '', appSecret: appSecret || '' },
132
+ { spaces: 2 },
133
+ );
134
+ try {
135
+ await fs.chmod(FACEBOOK_CONFIG_PATH, 0o600);
136
+ } catch {
137
+ /* ignore */
138
+ }
139
+ }
140
+
141
+ /** Set `withFacebookWebSignin` to the given boolean in the project's features.dart. */
142
+ async function setFacebookWebFlag(projectDir, value) {
143
+ const file = path.join(projectDir, 'lib', 'core', 'config', 'features.dart');
144
+ if (!(await fs.pathExists(file))) return { ok: false, missing: true };
145
+ let content = await fs.readFile(file, 'utf8');
146
+ const re = /const bool withFacebookWebSignin\s*=\s*(?:true|false)\s*;/;
147
+ if (!re.test(content)) return { ok: false, missing: true };
148
+ content = content.replace(re, `const bool withFacebookWebSignin = ${value};`);
149
+ await fs.writeFile(file, content, 'utf8');
150
+ return { ok: true };
151
+ }
152
+
153
+ module.exports = {
154
+ FACEBOOK_CONFIG_PATH,
155
+ readFacebookCurrent,
156
+ isFacebookPlaceholder,
157
+ readFacebookState,
158
+ writeFacebookCredentials,
159
+ loadFacebookCreds,
160
+ saveFacebookCreds,
161
+ setFacebookWebFlag,
162
+ };