kasy-cli 1.34.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 (141) hide show
  1. package/README.md +1 -1
  2. package/bin/kasy.js +24 -2
  3. package/docs/cli-reference.md +7 -7
  4. package/lib/commands/new.js +11 -9
  5. package/lib/commands/release-version.js +234 -0
  6. package/lib/commands/update.js +27 -0
  7. package/lib/scaffold/CHANGELOG.json +9 -0
  8. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
  9. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
  10. package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
  11. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  12. package/lib/scaffold/backends/firebase/setup-from-scratch.js +35 -21
  13. package/lib/scaffold/backends/patch-base-hashes.json +66 -0
  14. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
  15. package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
  16. package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
  17. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +82 -3
  18. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
  19. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  20. package/lib/scaffold/generate.js +53 -4
  21. package/lib/utils/i18n/messages-en.js +23 -0
  22. package/lib/utils/i18n/messages-es.js +23 -0
  23. package/lib/utils/i18n/messages-pt.js +23 -0
  24. package/package.json +5 -2
  25. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
  26. package/templates/firebase/AGENTS.md +83 -0
  27. package/templates/firebase/DESIGN_SYSTEM.md +37 -2
  28. package/templates/firebase/docs/auth-setup.en.md +2 -0
  29. package/templates/firebase/docs/auth-setup.es.md +2 -0
  30. package/templates/firebase/docs/auth-setup.pt.md +2 -0
  31. package/templates/firebase/firebase.json +56 -1
  32. package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
  33. package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
  34. package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
  35. package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
  36. package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
  37. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
  38. package/templates/firebase/lib/components/kasy_alert.dart +0 -1
  39. package/templates/firebase/lib/components/kasy_app_bar.dart +31 -16
  40. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
  41. package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
  42. package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
  43. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
  44. package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
  45. package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
  46. package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
  47. package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
  48. package/templates/firebase/lib/components/kasy_toast.dart +107 -41
  49. package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
  50. package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
  51. package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
  52. package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
  53. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
  54. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
  55. package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
  56. package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
  57. package/templates/firebase/lib/core/guards/guard.dart +16 -2
  58. package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
  59. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
  60. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +5 -3
  61. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
  62. package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
  63. package/templates/firebase/lib/core/states/logout_action.dart +5 -1
  64. package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
  65. package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
  66. package/templates/firebase/lib/core/theme/texts.dart +90 -57
  67. package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
  68. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
  69. package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
  70. package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
  71. package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
  72. package/templates/firebase/lib/core/web_screen_width.dart +15 -0
  73. package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
  74. package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
  75. package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
  76. package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
  77. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +1 -2
  78. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
  79. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
  80. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
  81. package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
  82. package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
  83. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +205 -0
  84. package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
  85. package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
  86. package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
  87. package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
  88. package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
  89. package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
  90. package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
  91. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
  92. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
  93. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +59 -0
  94. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
  95. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
  96. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
  97. package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
  98. package/templates/firebase/lib/features/home/home_components_page.dart +4 -3
  99. package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
  100. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +154 -56
  101. package/templates/firebase/lib/features/home/home_page.dart +4 -0
  102. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +8 -3
  103. package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
  104. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -1
  105. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +8 -3
  106. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
  107. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
  108. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
  109. package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
  110. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +43 -15
  111. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
  112. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
  113. package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
  114. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
  115. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
  116. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
  117. package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
  118. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
  119. package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
  120. package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
  121. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
  122. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
  123. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
  124. package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
  125. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
  126. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
  127. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
  128. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
  129. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
  130. package/templates/firebase/lib/i18n/en.i18n.json +49 -3
  131. package/templates/firebase/lib/i18n/es.i18n.json +49 -3
  132. package/templates/firebase/lib/i18n/pt.i18n.json +49 -3
  133. package/templates/firebase/lib/main.dart +11 -2
  134. package/templates/firebase/lib/router.dart +92 -13
  135. package/templates/firebase/pubspec.yaml +1 -1
  136. package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
  137. package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
  138. package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
  139. package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
  140. package/templates/firebase/web/index.html +162 -14
  141. package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
@@ -1,9 +1,12 @@
1
1
  import 'package:flutter/material.dart';
2
+ import 'package:kasy_kit/components/kasy_card.dart';
2
3
  import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
3
4
  import 'package:kasy_kit/core/theme/theme.dart';
4
- import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
5
5
 
6
- /// Here is just a simple content card
6
+ /// A tappable admin panel card. Uses the design-system [KasyCard] (elevated
7
+ /// surface, hairline border, soft shadow) with a compact title / description
8
+ /// and a trailing chevron, so admin tools read like the rest of the app
9
+ /// instead of a flat tinted block.
7
10
  class AdminPanelCard extends StatelessWidget {
8
11
  final VoidCallback onTap;
9
12
  final String title;
@@ -22,45 +25,46 @@ class AdminPanelCard extends StatelessWidget {
22
25
 
23
26
  @override
24
27
  Widget build(BuildContext context) {
25
- void handleActivate() {
26
- KasyHaptics.medium(context);
27
- onTap.call();
28
- }
29
-
30
- return KasyFocusRing(
31
- onActivate: handleActivate,
32
- borderRadius: BorderRadius.circular(KasyRadius.sm),
33
- child: InkWell(
34
- canRequestFocus: false,
35
- onTap: handleActivate,
36
- child: Card(
37
- color: backgroundColor ?? context.colors.primary.withValues(alpha: .15),
38
- margin: EdgeInsets.zero,
39
- elevation: 0,
40
- child: Padding(
41
- padding: const EdgeInsets.all(KasySpacing.md),
42
- child: Column(
43
- crossAxisAlignment: CrossAxisAlignment.start,
44
- spacing: 8,
45
- children: [
46
- Text(
47
- title,
48
- style: context.textTheme.headlineSmall?.copyWith(
49
- color: textColor ?? context.colors.onSurface,
28
+ return KasyCard(
29
+ onTap: () {
30
+ KasyHaptics.medium(context);
31
+ onTap();
32
+ },
33
+ color: backgroundColor,
34
+ borderRadius: KasyRadius.lgBorderRadius,
35
+ padding: const EdgeInsets.all(KasySpacing.md),
36
+ semanticLabel: title,
37
+ child: Row(
38
+ children: [
39
+ Expanded(
40
+ child: Column(
41
+ crossAxisAlignment: CrossAxisAlignment.start,
42
+ mainAxisSize: MainAxisSize.min,
43
+ children: [
44
+ Text(
45
+ title,
46
+ style: context.kasyTextTheme.cardTitle.copyWith(
47
+ color: textColor ?? context.colors.onSurface,
48
+ ),
50
49
  ),
51
- ),
52
- Text(
53
- description,
54
- style: context.textTheme.bodyMedium?.copyWith(
55
- color:
56
- textColor ??
57
- context.colors.onSurface.withValues(alpha: .6),
50
+ const SizedBox(height: 2),
51
+ Text(
52
+ description,
53
+ style: context.textTheme.bodySmall?.copyWith(
54
+ color: textColor ?? context.colors.muted,
55
+ height: 1.35,
56
+ ),
58
57
  ),
59
- ),
60
- ],
58
+ ],
59
+ ),
60
+ ),
61
+ const SizedBox(width: KasySpacing.sm),
62
+ Icon(
63
+ KasyIcons.arrowForwardIos,
64
+ size: KasyIconSize.xs,
65
+ color: context.colors.muted,
61
66
  ),
62
- ),
63
- ),
67
+ ],
64
68
  ),
65
69
  );
66
70
  }
@@ -140,12 +140,16 @@ class SettingsSwitchTile extends StatelessWidget {
140
140
  }
141
141
  }
142
142
 
143
- /// This widget is used to show a settings tile with an icon and a title
143
+ /// This widget is used to show a settings tile with an icon and a title.
144
+ /// Optionally shows a [trailingLabel] value before the chevron — mirrors the
145
+ /// pattern of [LanguageSwitcher] where the current value appears on the right.
144
146
  class SettingsTile extends StatelessWidget {
145
147
  final IconData icon;
146
148
  final String title;
147
149
  final SettingsTileOnTap onTap;
148
150
  final Color? iconBackgroundColor;
151
+ final String? trailingLabel;
152
+ final Widget? trailingWidget;
149
153
 
150
154
  const SettingsTile({
151
155
  super.key,
@@ -153,6 +157,8 @@ class SettingsTile extends StatelessWidget {
153
157
  required this.title,
154
158
  required this.onTap,
155
159
  this.iconBackgroundColor,
160
+ this.trailingLabel,
161
+ this.trailingWidget,
156
162
  });
157
163
 
158
164
  @override
@@ -184,7 +190,16 @@ class SettingsTile extends StatelessWidget {
184
190
  ),
185
191
  ),
186
192
  ),
187
- const SettingsListChevron(),
193
+ if (trailingLabel != null) ...[
194
+ Text(
195
+ trailingLabel!,
196
+ style: context.textTheme.bodyMedium?.copyWith(
197
+ color: context.colors.muted,
198
+ ),
199
+ ),
200
+ const SizedBox(width: KasySpacing.xs),
201
+ ],
202
+ trailingWidget ?? const SettingsListChevron(),
188
203
  ],
189
204
  ),
190
205
  );
@@ -44,6 +44,9 @@ sealed class SubscriptionEntity with _$SubscriptionEntity {
44
44
  @JsonKey(name: 'expiration_date')
45
45
  @TimestampConverter()
46
46
  DateTime? periodEndDate,
47
+ @JsonKey(name: 'trial_end')
48
+ @TimestampConverter()
49
+ DateTime? trialEnd,
47
50
  @JsonKey(name: 'status') required SubscriptionStatus status,
48
51
  // Origin of the subscription (apple/play/stripe/...). Nullable + tolerant
49
52
  // parsing so old records (no `store`) and future values never break.
@@ -1,4 +1,5 @@
1
1
  import 'package:flutter/material.dart';
2
+ import 'package:intl/intl.dart';
2
3
  import 'package:kasy_kit/core/data/models/subscription.dart';
3
4
  import 'package:kasy_kit/i18n/translations.g.dart';
4
5
 
@@ -121,15 +122,13 @@ class StripeProduct implements SubscriptionProduct {
121
122
  }
122
123
  }
123
124
 
124
- String _formatMoney(double value) {
125
- final amount = value.toStringAsFixed(2);
126
- if (currency == 'USD') return "\$$amount";
127
- if (currency == 'EUR') return "$amount€";
128
- return "$amount $currency";
129
- }
125
+ String _formatMoney(double value, {String? locale}) =>
126
+ NumberFormat.simpleCurrency(locale: locale, name: _currency.toUpperCase())
127
+ .format(value);
130
128
 
131
129
  @override
132
130
  String formattedPrice(BuildContext context) {
131
+ final locale = Localizations.localeOf(context).toString();
133
132
  final translatedDuration = switch (durationType) {
134
133
  DurationType.week => Translations.of(context).premium.duration_weekly,
135
134
  DurationType.year => Translations.of(context).premium.duration_annual,
@@ -139,16 +138,17 @@ class StripeProduct implements SubscriptionProduct {
139
138
  ).premium.duration_lifetime,
140
139
  _ => "",
141
140
  };
142
- return "$priceString $translatedDuration";
141
+ return "${_formatMoney(price, locale: locale)} $translatedDuration";
143
142
  }
144
143
 
145
144
  @override
146
145
  String pricePerMonth(BuildContext context) {
146
+ final locale = Localizations.localeOf(context).toString();
147
147
  final translatedDuration = Translations.of(
148
148
  context,
149
149
  ).premium.duration_monthly;
150
150
  if (durationType == DurationType.lifetime) {
151
- return priceString;
151
+ return _formatMoney(price, locale: locale);
152
152
  }
153
153
  final monthly = switch (durationType) {
154
154
  DurationType.year => price / 12,
@@ -157,14 +157,15 @@ class StripeProduct implements SubscriptionProduct {
157
157
  DurationType.week => (price * 4) / 12,
158
158
  _ => price,
159
159
  };
160
- return "${_formatMoney(monthly)}/$translatedDuration";
160
+ return "${_formatMoney(monthly, locale: locale)}/$translatedDuration";
161
161
  }
162
162
 
163
163
  @override
164
164
  String? pricePerYear(BuildContext context) {
165
+ final locale = Localizations.localeOf(context).toString();
165
166
  final translatedDuration = Translations.of(context).premium.duration_annual;
166
167
  if (durationType == DurationType.lifetime) {
167
- return priceString;
168
+ return _formatMoney(price, locale: locale);
168
169
  }
169
170
  final yearly = switch (durationType) {
170
171
  DurationType.year => price,
@@ -173,6 +174,6 @@ class StripeProduct implements SubscriptionProduct {
173
174
  DurationType.week => (price * 4) * 12,
174
175
  _ => price * 12,
175
176
  };
176
- return "${_formatMoney(yearly)}/$translatedDuration";
177
+ return "${_formatMoney(yearly, locale: locale)}/$translatedDuration";
177
178
  }
178
179
  }
@@ -86,11 +86,19 @@ class SubscriptionRepository implements OnStartService {
86
86
  final product = await _inAppSubscriptionApi.getFromProductId(
87
87
  entity!.skuId,
88
88
  );
89
-
89
+
90
+ // When the payment provider has no entitlement data (e.g. Stripe returns
91
+ // an empty list), synthesize one from the backend entity so the UI can
92
+ // show trial status, expiration date and renewal info.
93
+ final effectiveEntitlements =
94
+ (entitlements == null || entitlements.isEmpty)
95
+ ? _entitlementsFromEntity(entity)
96
+ : entitlements;
97
+
90
98
  return switch(subscription) {
91
99
  final SubscriptionStateData active => active.copyWith(
92
100
  activeOffer: product,
93
- entitlements: entitlements,
101
+ entitlements: effectiveEntitlements,
94
102
  ),
95
103
  _ => subscription,
96
104
  };
@@ -109,6 +117,21 @@ class SubscriptionRepository implements OnStartService {
109
117
  return subscription;
110
118
  }
111
119
 
120
+ List<Entitlement> _entitlementsFromEntity(SubscriptionEntity entity) {
121
+ final now = DateTime.now();
122
+ final trialEnd = entity.trialEnd;
123
+ final isInTrial = trialEnd != null && trialEnd.isAfter(now);
124
+ final willRenew = entity.status != SubscriptionStatus.CANCELLED;
125
+ return [
126
+ Entitlement(
127
+ identifier: entity.skuId,
128
+ isInTrial: isInTrial,
129
+ willRenew: willRenew,
130
+ expirationDate: isInTrial ? trialEnd : entity.periodEndDate,
131
+ ),
132
+ ];
133
+ }
134
+
112
135
  // We can have multiple offers (BASIC MONTH, BASIC YEAR, GOLD MONTH, GOLD YEAR, ...)
113
136
  Future<List<SubscriptionProduct>> getOffers({String? offerId}) {
114
137
  return _inAppSubscriptionApi.getOffers(offerId);