kasy-cli 1.31.14 → 1.32.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 (96) hide show
  1. package/lib/commands/new.js +15 -1
  2. package/lib/scaffold/CHANGELOG.json +9 -0
  3. package/lib/scaffold/backends/api/patch/README.md +87 -2
  4. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +34 -0
  5. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  6. package/lib/scaffold/backends/firebase/setup-from-scratch.js +22 -0
  7. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +26 -22
  8. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -11
  9. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +3 -1
  10. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +60 -3
  11. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +12 -0
  12. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  13. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +3 -2
  14. package/lib/scaffold/generate.js +1 -1
  15. package/lib/scaffold/shared/generator-utils.js +22 -3
  16. package/lib/utils/i18n/messages-en.js +2 -0
  17. package/lib/utils/i18n/messages-es.js +2 -0
  18. package/lib/utils/i18n/messages-pt.js +2 -0
  19. package/package.json +2 -2
  20. package/templates/firebase/docs/auth-setup.en.md +7 -1
  21. package/templates/firebase/docs/auth-setup.es.md +7 -1
  22. package/templates/firebase/docs/auth-setup.pt.md +7 -1
  23. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +79 -5
  24. package/templates/firebase/lib/components/kasy_accordion.dart +2 -2
  25. package/templates/firebase/lib/components/kasy_alert.dart +1 -1
  26. package/templates/firebase/lib/components/kasy_app_bar.dart +3 -3
  27. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +1 -1
  28. package/templates/firebase/lib/components/kasy_button.dart +8 -8
  29. package/templates/firebase/lib/components/kasy_chip.dart +1 -1
  30. package/templates/firebase/lib/components/kasy_date_picker.dart +26 -21
  31. package/templates/firebase/lib/components/kasy_dialog.dart +2 -2
  32. package/templates/firebase/lib/components/kasy_sidebar.dart +2 -2
  33. package/templates/firebase/lib/components/kasy_tabs.dart +2 -2
  34. package/templates/firebase/lib/components/kasy_text_area.dart +37 -5
  35. package/templates/firebase/lib/components/kasy_text_field.dart +77 -16
  36. package/templates/firebase/lib/components/kasy_toast.dart +1 -1
  37. package/templates/firebase/lib/components/kasy_web_header.dart +4 -3
  38. package/templates/firebase/lib/core/config/features.dart +13 -0
  39. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +21 -0
  40. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +1 -1
  41. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +1 -1
  42. package/templates/firebase/lib/core/theme/icon_sizes.dart +47 -0
  43. package/templates/firebase/lib/core/theme/shadows.dart +13 -0
  44. package/templates/firebase/lib/core/theme/texts.dart +32 -0
  45. package/templates/firebase/lib/core/theme/theme.dart +2 -0
  46. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -0
  47. package/templates/firebase/lib/core/web_viewport_scale.dart +23 -4
  48. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +1 -1
  49. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -1
  50. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +1 -1
  51. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -1
  52. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +3 -1
  53. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +36 -14
  54. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +27 -11
  55. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +1 -1
  56. package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +1 -1
  57. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +1 -1
  58. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -1
  59. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +22 -3
  60. package/templates/firebase/lib/features/home/home_image_grid.dart +1 -1
  61. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +2 -2
  62. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +6 -1
  63. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +35 -38
  64. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +1 -1
  65. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +1 -1
  66. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +3 -3
  67. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +1 -1
  68. package/templates/firebase/lib/features/settings/settings_page.dart +264 -307
  69. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
  70. package/templates/firebase/lib/features/settings/ui/components/edit_name_sheet.dart +115 -0
  71. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +2 -2
  72. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +1 -1
  73. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +13 -5
  74. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  75. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +7 -1
  76. package/templates/firebase/lib/features/subscriptions/providers/premium_page_provider.dart +11 -3
  77. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -1
  78. package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +1 -1
  79. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +2 -2
  80. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +1 -1
  81. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +1 -1
  82. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +3 -3
  83. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +1 -1
  84. package/templates/firebase/lib/i18n/en.i18n.json +8 -0
  85. package/templates/firebase/lib/i18n/es.i18n.json +8 -0
  86. package/templates/firebase/lib/i18n/pt.i18n.json +8 -0
  87. package/templates/firebase/pubspec.yaml +0 -1
  88. package/templates/firebase/web/stripe_success.html +64 -26
  89. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  90. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  91. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  92. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  93. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  94. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  95. package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +0 -19
  96. package/templates/firebase/login-redesign-preview.png +0 -0
@@ -103,7 +103,7 @@ class _EditableUserAvatarState extends ConsumerState<EditableUserAvatar> {
103
103
  ),
104
104
  child: const Icon(
105
105
  KasyIcons.cameraAlt,
106
- size: 12,
106
+ size: KasyIconSize.xxs,
107
107
  color: Colors.white,
108
108
  ),
109
109
  ),
@@ -336,7 +336,7 @@ class _BottomSheetTile extends StatelessWidget {
336
336
  child: Row(
337
337
  children: [
338
338
  if (icon != null) ...[
339
- Icon(icon, size: 22, color: fg),
339
+ Icon(icon, size: KasyIconSize.lg, color: fg),
340
340
  const SizedBox(width: KasySpacing.sm),
341
341
  ],
342
342
  Text(
@@ -0,0 +1,115 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+ import 'package:kasy_kit/components/components.dart';
4
+ import 'package:kasy_kit/core/data/repositories/user_repository.dart';
5
+ import 'package:kasy_kit/core/states/user_state_notifier.dart';
6
+ import 'package:kasy_kit/i18n/translations.g.dart';
7
+
8
+ /// Opens the "edit name" bottom sheet and, on success, shows a confirmation
9
+ /// toast on the calling [context]. The email is intentionally read-only — only
10
+ /// the display name can be changed here.
11
+ Future<void> showEditNameSheet(
12
+ BuildContext context, {
13
+ required String userId,
14
+ required String email,
15
+ required String currentName,
16
+ }) async {
17
+ final bool? saved = await showKasyBottomSheet<bool>(
18
+ context: context,
19
+ isScrollControlled: true,
20
+ builder: (_) => _EditNameSheet(
21
+ userId: userId,
22
+ email: email,
23
+ initialName: currentName,
24
+ ),
25
+ );
26
+ if (saved == true && context.mounted) {
27
+ showKasyToast(
28
+ context,
29
+ title: context.t.settings.edit_name_success,
30
+ tone: KasyToastTone.success,
31
+ );
32
+ }
33
+ }
34
+
35
+ class _EditNameSheet extends ConsumerStatefulWidget {
36
+ final String userId;
37
+ final String email;
38
+ final String initialName;
39
+
40
+ const _EditNameSheet({
41
+ required this.userId,
42
+ required this.email,
43
+ required this.initialName,
44
+ });
45
+
46
+ @override
47
+ ConsumerState<_EditNameSheet> createState() => _EditNameSheetState();
48
+ }
49
+
50
+ class _EditNameSheetState extends ConsumerState<_EditNameSheet> {
51
+ late final TextEditingController _controller =
52
+ TextEditingController(text: widget.initialName);
53
+ bool _saving = false;
54
+
55
+ @override
56
+ void dispose() {
57
+ _controller.dispose();
58
+ super.dispose();
59
+ }
60
+
61
+ Future<void> _save() async {
62
+ final String name = _controller.text.trim();
63
+ if (name.isEmpty || _saving) return;
64
+ setState(() => _saving = true);
65
+ final tr = context.t.settings;
66
+ try {
67
+ await ref.read(userRepositoryProvider).updateEmailAndName(
68
+ userId: widget.userId,
69
+ email: widget.email,
70
+ name: name,
71
+ );
72
+ await ref.read(userStateNotifierProvider.notifier).refresh();
73
+ if (!mounted) return;
74
+ // The success toast is shown by the launcher on the page context, since
75
+ // this sheet's context is gone right after the pop.
76
+ Navigator.of(context).pop(true);
77
+ } catch (_) {
78
+ if (!mounted) return;
79
+ setState(() => _saving = false);
80
+ showKasyToast(context, title: tr.edit_name_error, tone: KasyToastTone.danger);
81
+ }
82
+ }
83
+
84
+ @override
85
+ Widget build(BuildContext context) {
86
+ final tr = context.t.settings;
87
+ return KasyBottomSheet(
88
+ title: tr.edit_name_title,
89
+ addKeyboardInset: true,
90
+ body: KasyTextField(
91
+ controller: _controller,
92
+ label: tr.name_label,
93
+ hint: tr.edit_name_hint,
94
+ autofocus: true,
95
+ textCapitalization: TextCapitalization.words,
96
+ textInputAction: TextInputAction.done,
97
+ onSubmitted: (_) => _save(),
98
+ ),
99
+ actions: [
100
+ KasyButton(
101
+ label: tr.edit_name_save,
102
+ expand: true,
103
+ isLoading: _saving,
104
+ onPressed: _save,
105
+ ),
106
+ KasyButton(
107
+ label: tr.edit_name_cancel,
108
+ variant: KasyButtonVariant.ghost,
109
+ expand: true,
110
+ onPressed: _saving ? null : () => Navigator.of(context).pop(),
111
+ ),
112
+ ],
113
+ );
114
+ }
115
+ }
@@ -30,14 +30,14 @@ class LanguageSwitcher extends ConsumerWidget {
30
30
  children: <Widget>[
31
31
  Icon(
32
32
  KasyIcons.language,
33
- size: 21,
33
+ size: KasyIconSize.rowLeading,
34
34
  color: context.colors.onSurface,
35
35
  ),
36
36
  const SizedBox(width: KasySpacing.sm),
37
37
  Expanded(
38
38
  child: Text(
39
39
  context.t.settings.language_title,
40
- style: context.textTheme.titleMedium?.copyWith(
40
+ style: context.textTheme.titleSmall?.copyWith(
41
41
  color: context.colors.onSurface,
42
42
  ),
43
43
  ),
@@ -39,7 +39,7 @@ class SettingsBottomSheetOptionTile extends StatelessWidget {
39
39
  leading!,
40
40
  const SizedBox(width: KasySpacing.sm),
41
41
  ] else if (icon != null) ...<Widget>[
42
- Icon(icon, size: 22, color: fg),
42
+ Icon(icon, size: KasyIconSize.lg, color: fg),
43
43
  const SizedBox(width: KasySpacing.sm),
44
44
  ],
45
45
  Expanded(child: labelWidget),
@@ -42,7 +42,7 @@ class SettingsIconBadge extends StatelessWidget {
42
42
  class SettingsListChevron extends StatelessWidget {
43
43
  const SettingsListChevron({super.key});
44
44
 
45
- static const double _iconSize = 18;
45
+ static const double _iconSize = KasyIconSize.rowTrailing;
46
46
 
47
47
  @override
48
48
  Widget build(BuildContext context) {
@@ -97,7 +97,11 @@ class SettingsSwitchTile extends StatelessWidget {
97
97
  if (iconBackgroundColor != null)
98
98
  SettingsIconBadge(icon: icon, color: iconBackgroundColor!)
99
99
  else
100
- Icon(icon, size: 21, color: context.colors.onSurface),
100
+ Icon(
101
+ icon,
102
+ size: KasyIconSize.rowLeading,
103
+ color: context.colors.onSurface,
104
+ ),
101
105
  const SizedBox(width: KasySpacing.sm),
102
106
  Expanded(
103
107
  child: Column(
@@ -106,7 +110,7 @@ class SettingsSwitchTile extends StatelessWidget {
106
110
  children: <Widget>[
107
111
  Text(
108
112
  title,
109
- style: context.textTheme.titleMedium?.copyWith(
113
+ style: context.textTheme.titleSmall?.copyWith(
110
114
  color: context.colors.onSurface,
111
115
  ),
112
116
  ),
@@ -166,12 +170,16 @@ class SettingsTile extends StatelessWidget {
166
170
  if (iconBackgroundColor != null)
167
171
  SettingsIconBadge(icon: icon, color: iconBackgroundColor!)
168
172
  else
169
- Icon(icon, size: 21, color: context.colors.onSurface),
173
+ Icon(
174
+ icon,
175
+ size: KasyIconSize.rowLeading,
176
+ color: context.colors.onSurface,
177
+ ),
170
178
  const SizedBox(width: KasySpacing.sm),
171
179
  Expanded(
172
180
  child: Text(
173
181
  title,
174
- style: context.textTheme.titleMedium?.copyWith(
182
+ style: context.textTheme.titleSmall?.copyWith(
175
183
  color: context.colors.onSurface,
176
184
  ),
177
185
  ),
@@ -35,6 +35,7 @@ class StripeBackendApi {
35
35
  String? successUrl,
36
36
  String? cancelUrl,
37
37
  String? locale,
38
+ bool? allowPromoCodes,
38
39
  }) async {
39
40
  final res = await _functions
40
41
  .httpsCallable('stripeFunctions-createCheckoutSession')
@@ -43,15 +44,24 @@ class StripeBackendApi {
43
44
  if (successUrl != null) 'successUrl': successUrl,
44
45
  if (cancelUrl != null) 'cancelUrl': cancelUrl,
45
46
  if (locale != null) 'locale': locale,
47
+ if (allowPromoCodes != null) 'allowPromoCodes': allowPromoCodes,
46
48
  });
47
49
  return (res.data as Map)['url'] as String;
48
50
  }
49
51
 
50
52
  /// Create a Customer Portal session (manage / cancel) and return its URL.
51
- Future<String> createPortalSession({String? returnUrl}) async {
53
+ /// Pass [planSwitching] = true to auto-configure the portal with
54
+ /// upgrade/downgrade support (no manual Stripe dashboard setup needed).
55
+ Future<String> createPortalSession({
56
+ String? returnUrl,
57
+ bool? planSwitching,
58
+ }) async {
52
59
  final res = await _functions
53
60
  .httpsCallable('stripeFunctions-createPortalSession')
54
- .call({if (returnUrl != null) 'returnUrl': returnUrl});
61
+ .call({
62
+ if (returnUrl != null) 'returnUrl': returnUrl,
63
+ if (planSwitching != null) 'planSwitching': planSwitching,
64
+ });
55
65
  return (res.data as Map)['url'] as String;
56
66
  }
57
67
  }
@@ -1,3 +1,4 @@
1
+ import 'package:kasy_kit/core/config/features.dart';
1
2
  import 'package:kasy_kit/core/data/models/entitlement.dart';
2
3
  import 'package:kasy_kit/core/data/models/subscription.dart';
3
4
  import 'package:kasy_kit/features/subscriptions/api/entities/subscription_entity.dart';
@@ -39,7 +40,10 @@ class StripePaymentApi implements SubscriptionPaymentApi {
39
40
  // tab. The original app tab keeps polling and flips to premium via the
40
41
  // webhook. On cancel we send the user back to the app where they were.
41
42
  final appUrl = Uri.base.toString();
42
- final successUrl = '${Uri.base.origin}/stripe_success.html';
43
+ // Pass the in-app language so the success page renders in the locale the
44
+ // user picked here, not just the browser's (?lang= takes priority there).
45
+ final lang = LocaleSettings.instance.currentLocale.languageCode;
46
+ final successUrl = '${Uri.base.origin}/stripe_success.html?lang=$lang';
43
47
  final url = await _backend.createCheckoutSession(
44
48
  priceId: product.skuId,
45
49
  successUrl: successUrl,
@@ -48,6 +52,7 @@ class StripePaymentApi implements SubscriptionPaymentApi {
48
52
  // deliver subscription notifications in the right language (on web there
49
53
  // is no registered device to read the locale from).
50
54
  locale: LocaleSettings.instance.currentLocale.languageCode,
55
+ allowPromoCodes: withStripePromoCodes,
51
56
  );
52
57
  await _open(url);
53
58
  // Checkout is now open in a new tab. Payment is NOT confirmed yet — the
@@ -60,6 +65,7 @@ class StripePaymentApi implements SubscriptionPaymentApi {
60
65
  Future<void> unsubscribe(SubscriptionStore? origin) async {
61
66
  final url = await _backend.createPortalSession(
62
67
  returnUrl: Uri.base.toString(),
68
+ planSwitching: withStripePlanSwitching,
63
69
  );
64
70
  await _open(url);
65
71
  }
@@ -269,10 +269,18 @@ class PremiumStateNotifier extends _$PremiumStateNotifier {
269
269
  }
270
270
 
271
271
  try {
272
- await _subscriptionRepository.restorePurchase();
272
+ // Store-level restore: RevenueCat on native (asks Apple/Google to restore
273
+ // purchases), a no-op on web (Stripe status lives server-side). Best-effort
274
+ // — RevenueCat throws when there is nothing to restore, but the backend
275
+ // re-read below is the source of truth and decides the message, so both
276
+ // web and native end on the same friendly "nothing to restore" path.
277
+ try {
278
+ await _subscriptionRepository.restorePurchase();
279
+ } catch (_) {
280
+ // Ignore: fall through to the backend check.
281
+ }
273
282
 
274
- // restorePurchase is a no-op on web (Stripe status lives server-side), so
275
- // we re-read the backend to learn the real state: the webhook may have
283
+ // Re-read the backend to learn the real state: the webhook may have
276
284
  // already written the subscription. Only flip to active when the backend
277
285
  // actually confirms it — otherwise we'd show a false "restored" success.
278
286
  final userId = _userState.user.idOrNull;
@@ -280,7 +280,7 @@ class _BasicPaywallRowState extends ConsumerState<PaywallRow> {
280
280
  children: [
281
281
  Icon(
282
282
  KasyIcons.security,
283
- size: 14,
283
+ size: KasyIconSize.xs,
284
284
  color: context.colors.primary,
285
285
  ),
286
286
  const SizedBox(width: KasySpacing.xs),
@@ -312,7 +312,7 @@ class _ComparisonTable extends StatelessWidget {
312
312
  isAvailable
313
313
  ? KasyIcons.check
314
314
  : KasyIcons.close,
315
- size: 20,
315
+ size: KasyIconSize.lg,
316
316
  color: isAvailable
317
317
  ? (isPremium ? context.colors.primary : context.colors.success)
318
318
  : context.colors.error,
@@ -28,7 +28,7 @@ class FeatureLine extends StatelessWidget {
28
28
  Icon(
29
29
  icon,
30
30
  color: context.colors.onBackground,
31
- size: 16,
31
+ size: KasyIconSize.sm,
32
32
  ),
33
33
  if (icon != null)
34
34
  const SizedBox(width: KasySpacing.smd),
@@ -93,7 +93,7 @@ class FeatureWithTwoLines extends StatelessWidget {
93
93
  Icon(
94
94
  icon,
95
95
  color: context.colors.onBackground,
96
- size: 24,
96
+ size: KasyIconSize.xl,
97
97
  ),
98
98
  const SizedBox(width: KasySpacing.smd),
99
99
  Flexible(
@@ -58,7 +58,7 @@ class AppCloseButton extends StatelessWidget {
58
58
  child: Icon(
59
59
  KasyIcons.close,
60
60
  color: context.colors.background,
61
- size: 21,
61
+ size: KasyIconSize.lg,
62
62
  ),
63
63
  ),
64
64
  ),
@@ -19,7 +19,7 @@ class PremiumFeature extends StatelessWidget {
19
19
  Icon(
20
20
  KasyIcons.check,
21
21
  color: context.colors.onPrimary,
22
- size: 24,
22
+ size: KasyIconSize.xl,
23
23
  ),
24
24
  const SizedBox(width: KasySpacing.md),
25
25
  Expanded(
@@ -230,8 +230,8 @@ class _SelectableColState extends State<SelectableCol>
230
230
  Padding(
231
231
  padding: const EdgeInsets.only(bottom: 2), // pixel-level icon alignment — intentional exception
232
232
  child: Icon(
233
- widget.icon,
234
- size: 24,
233
+ widget.icon,
234
+ size: KasyIconSize.xl,
235
235
  color: switch(widget.brightness) {
236
236
  Brightness.light => context.colors.onBackground,
237
237
  _ => context.colors.background,
@@ -374,7 +374,7 @@ class RoundRadioBox extends StatelessWidget {
374
374
  opacity: iconOpacity,
375
375
  child: Transform.scale(
376
376
  scale: iconSize,
377
- child: Icon(icon, color: context.colors.onPrimary, size: 21),
377
+ child: Icon(icon, color: context.colors.onPrimary, size: KasyIconSize.lg),
378
378
  ),
379
379
  )
380
380
  : const SizedBox.shrink(),
@@ -433,7 +433,7 @@ class RoundRadioBox extends StatelessWidget {
433
433
  opacity: iconOpacity,
434
434
  child: Transform.scale(
435
435
  scale: iconSize,
436
- child: Icon(icon, color: context.colors.background, size: 16),
436
+ child: Icon(icon, color: context.colors.background, size: KasyIconSize.sm),
437
437
  ),
438
438
  )
439
439
  : const SizedBox.shrink(),
@@ -495,6 +495,14 @@
495
495
  "my_account": "My account",
496
496
  "not_signed_in": "Not signed in",
497
497
  "register": "Register",
498
+ "name_label": "Name",
499
+ "email_label": "Email",
500
+ "edit_name_title": "Edit name",
501
+ "edit_name_hint": "Your name",
502
+ "edit_name_save": "Save",
503
+ "edit_name_cancel": "Cancel",
504
+ "edit_name_success": "Name updated",
505
+ "edit_name_error": "Couldn't update your name. Please try again.",
498
506
  "reminders": "Reminders",
499
507
  "admin_panel": "Admin Panel",
500
508
  "admin_debug_section_label": "ADMIN (DEBUG ONLY)",
@@ -495,6 +495,14 @@
495
495
  "my_account": "Mi cuenta",
496
496
  "not_signed_in": "No conectado",
497
497
  "register": "Registrarse",
498
+ "name_label": "Nombre",
499
+ "email_label": "Correo electrónico",
500
+ "edit_name_title": "Editar nombre",
501
+ "edit_name_hint": "Tu nombre",
502
+ "edit_name_save": "Guardar",
503
+ "edit_name_cancel": "Cancelar",
504
+ "edit_name_success": "Nombre actualizado",
505
+ "edit_name_error": "No se pudo actualizar tu nombre. Inténtalo de nuevo.",
498
506
  "reminders": "Recordatorios",
499
507
  "admin_panel": "Panel de Administración",
500
508
  "admin_debug_section_label": "ADMIN (SOLO DEBUG)",
@@ -495,6 +495,14 @@
495
495
  "my_account": "Minha conta",
496
496
  "not_signed_in": "Não conectado",
497
497
  "register": "Cadastrar",
498
+ "name_label": "Nome",
499
+ "email_label": "Email",
500
+ "edit_name_title": "Editar nome",
501
+ "edit_name_hint": "Seu nome",
502
+ "edit_name_save": "Salvar",
503
+ "edit_name_cancel": "Cancelar",
504
+ "edit_name_success": "Nome atualizado",
505
+ "edit_name_error": "Não foi possível atualizar seu nome. Tente novamente.",
498
506
  "reminders": "Lembretes",
499
507
  "admin_panel": "Painel Admin",
500
508
  "admin_debug_section_label": "ADMIN (SÓ EM DEBUG)",
@@ -34,7 +34,6 @@ dependencies:
34
34
  another_flushbar: ^1.12.32
35
35
  background_fetch: ^1.5.0
36
36
  bart: ^1.4.1
37
- better_skeleton: ^0.1.0
38
37
  cloud_firestore: ^6.1.2
39
38
  cloud_functions: ^6.0.6
40
39
  cross_file: ^0.3.5+2
@@ -4,84 +4,112 @@
4
4
 
5
5
  Stripe redirects the checkout tab here (success_url) after a successful
6
6
  payment. It is intentionally a tiny standalone page — NOT the full Flutter
7
- app — so the user does not end up with two heavy app tabs open. The original
8
- app tab keeps polling and flips to premium on its own (via the webhook), so
9
- here we just congratulate the user and let them close this tab / return.
7
+ app — so the user does not end up with two heavy app tabs open, and so the
8
+ "return to the app" button can reliably window.close() this tab (only a light
9
+ page opened via window.open is allowed to close itself). The original app tab
10
+ keeps polling and flips to premium on its own (via the webhook).
10
11
 
11
- Localized client-side from navigator.language (pt / es / en).
12
+ Styled to match the kit's design system (Inter + the same color tokens as the
13
+ Flutter app). Localized from the ?lang= the app appends (the language picked
14
+ IN the app), falling back to the browser language.
12
15
  -->
13
16
  <html lang="en">
14
17
  <head>
15
18
  <meta charset="UTF-8" />
16
19
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
17
20
  <title>Payment complete</title>
21
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
22
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
23
+ <link
24
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
25
+ rel="stylesheet"
26
+ />
18
27
  <style>
28
+ /* Kit design tokens (mirror lib/core/theme/colors.dart). */
19
29
  :root {
20
30
  --accent: #0485F7;
21
- --success: #16A34A;
22
- --bg: #F6F8FB;
31
+ --accent-fg: #FFFFFF;
32
+ --success: #17C964;
33
+ --bg: #F5F5F5;
23
34
  --card: #FFFFFF;
24
- --text: #0B1524;
25
- --muted: #5B6776;
26
- --border: rgba(11, 21, 36, 0.08);
35
+ --text: #18181B;
36
+ --muted: #71717A;
37
+ --border: rgba(24, 24, 27, 0.10);
38
+ --shadow: 0 12px 40px rgba(24, 24, 27, 0.10);
27
39
  }
28
40
  @media (prefers-color-scheme: dark) {
29
41
  :root {
30
- --bg: #0B1220;
31
- --card: #131C2B;
32
- --text: #F3F6FB;
33
- --muted: #9AA7B8;
34
- --border: rgba(255, 255, 255, 0.10);
42
+ --bg: #060607;
43
+ --card: #18181B;
44
+ --text: #FCFCFC;
45
+ --muted: #A1A1AA;
46
+ --border: rgba(255, 255, 255, 0.08);
47
+ --shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
35
48
  }
36
49
  }
37
50
  * { box-sizing: border-box; }
38
51
  html, body { height: 100%; margin: 0; }
39
52
  body {
40
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
41
- Helvetica, Arial, sans-serif;
53
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
54
+ Roboto, Helvetica, Arial, sans-serif;
42
55
  background: var(--bg);
43
56
  color: var(--text);
44
57
  display: flex;
45
58
  align-items: center;
46
59
  justify-content: center;
47
60
  padding: 24px;
61
+ -webkit-font-smoothing: antialiased;
62
+ -moz-osx-font-smoothing: grayscale;
48
63
  }
49
64
  .card {
50
65
  width: 100%;
51
66
  max-width: 420px;
52
67
  background: var(--card);
53
68
  border: 1px solid var(--border);
54
- border-radius: 20px;
69
+ border-radius: 24px;
55
70
  padding: 40px 32px;
56
71
  text-align: center;
57
- box-shadow: 0 12px 40px rgba(11, 21, 36, 0.08);
72
+ box-shadow: var(--shadow);
58
73
  }
59
74
  .check {
60
75
  width: 72px;
61
76
  height: 72px;
62
77
  margin: 0 auto 24px;
63
78
  border-radius: 50%;
64
- background: rgba(22, 163, 74, 0.12);
79
+ background: rgba(23, 201, 100, 0.12);
65
80
  display: flex;
66
81
  align-items: center;
67
82
  justify-content: center;
68
83
  }
69
- .check svg { width: 38px; height: 38px; stroke: var(--success); }
70
- h1 { font-size: 22px; font-weight: 700; margin: 0 0 10px; letter-spacing: -0.4px; }
71
- p { font-size: 15px; line-height: 1.5; color: var(--muted); margin: 0 0 28px; }
84
+ .check svg { width: 36px; height: 36px; stroke: var(--success); }
85
+ h1 {
86
+ font-size: 24px;
87
+ font-weight: 700;
88
+ line-height: 32px;
89
+ letter-spacing: -0.2px;
90
+ margin: 0 0 10px;
91
+ }
92
+ p {
93
+ font-size: 16px;
94
+ line-height: 24px;
95
+ color: var(--muted);
96
+ margin: 0 0 28px;
97
+ }
72
98
  button {
73
99
  width: 100%;
74
100
  border: none;
75
- border-radius: 12px;
101
+ border-radius: 14px;
76
102
  padding: 14px 20px;
77
- font-size: 15px;
103
+ font-family: inherit;
104
+ font-size: 16px;
78
105
  font-weight: 600;
79
- color: #FFFFFF;
106
+ color: var(--accent-fg);
80
107
  background: var(--accent);
81
108
  cursor: pointer;
82
109
  transition: opacity 0.15s ease;
83
110
  }
84
111
  button:hover { opacity: 0.92; }
112
+ button:active { opacity: 0.85; }
85
113
  </style>
86
114
  </head>
87
115
  <body>
@@ -119,7 +147,17 @@
119
147
  },
120
148
  };
121
149
 
122
- var lang = (navigator.language || "en").slice(0, 2).toLowerCase();
150
+ // Prefer the language the app passed (?lang=xx) — the one the user picked
151
+ // IN the app — then fall back to the browser language.
152
+ function pickLang() {
153
+ try {
154
+ var q = new URLSearchParams(window.location.search).get("lang");
155
+ if (q) return q.slice(0, 2).toLowerCase();
156
+ } catch (e) {}
157
+ return (navigator.language || "en").slice(0, 2).toLowerCase();
158
+ }
159
+
160
+ var lang = pickLang();
123
161
  var t = I18N[lang] || I18N.en;
124
162
  document.documentElement.lang = lang in I18N ? lang : "en";
125
163
  document.title = t.doc;
@@ -1,34 +0,0 @@
1
- import 'package:flutter/material.dart';
2
- import 'package:flutter_animate/flutter_animate.dart';
3
-
4
- class MoveFadeAnim extends StatelessWidget {
5
- final int? delayInMs;
6
- final Widget child;
7
-
8
- const MoveFadeAnim({
9
- super.key,
10
- required this.child,
11
- this.delayInMs,
12
- });
13
-
14
- @override
15
- Widget build(BuildContext context) {
16
- return Animate(
17
- effects: [
18
- FadeEffect(
19
- delay: Duration(milliseconds: delayInMs ?? 0),
20
- duration: const Duration(milliseconds: 200),
21
- curve: Curves.easeIn,
22
- ),
23
- MoveEffect(
24
- delay: Duration(milliseconds: delayInMs ?? 0),
25
- duration: const Duration(milliseconds: 450),
26
- curve: Curves.easeOut,
27
- begin: const Offset(0, 50),
28
- end: Offset.zero,
29
- ),
30
- ],
31
- child: child,
32
- );
33
- }
34
- }