kasy-cli 1.31.14 → 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kasy.js +42 -0
- package/lib/commands/apple-web.js +222 -0
- package/lib/commands/configure.js +3 -91
- package/lib/commands/doctor.js +20 -0
- package/lib/commands/facebook.js +189 -0
- package/lib/commands/new.js +65 -3
- package/lib/scaffold/CHANGELOG.json +27 -0
- package/lib/scaffold/backends/api/patch/README.md +87 -2
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +34 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +186 -0
- package/lib/scaffold/backends/supabase/deploy.js +92 -0
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +26 -22
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -11
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +3 -1
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +60 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +22 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +3 -2
- package/lib/scaffold/generate.js +1 -1
- package/lib/scaffold/shared/generator-utils.js +34 -3
- package/lib/utils/apple-web.js +147 -0
- package/lib/utils/facebook.js +162 -0
- package/lib/utils/i18n/messages-en.js +64 -0
- package/lib/utils/i18n/messages-es.js +64 -0
- package/lib/utils/i18n/messages-pt.js +64 -0
- package/package.json +2 -2
- package/templates/firebase/AGENTS.md +87 -0
- package/templates/firebase/CLAUDE.md +16 -0
- package/templates/firebase/DESIGN_SYSTEM.md +234 -0
- package/templates/firebase/docs/auth-setup.en.md +7 -1
- package/templates/firebase/docs/auth-setup.es.md +7 -1
- package/templates/firebase/docs/auth-setup.pt.md +7 -1
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +79 -5
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_accordion.dart +2 -2
- package/templates/firebase/lib/components/kasy_alert.dart +1 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +7 -4
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +1 -1
- package/templates/firebase/lib/components/kasy_button.dart +8 -8
- package/templates/firebase/lib/components/kasy_chip.dart +1 -1
- package/templates/firebase/lib/components/kasy_date_picker.dart +26 -21
- package/templates/firebase/lib/components/kasy_dialog.dart +2 -2
- package/templates/firebase/lib/components/kasy_screen.dart +114 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +2 -2
- package/templates/firebase/lib/components/kasy_tabs.dart +2 -2
- package/templates/firebase/lib/components/kasy_text_area.dart +37 -5
- package/templates/firebase/lib/components/kasy_text_field.dart +77 -16
- package/templates/firebase/lib/components/kasy_toast.dart +39 -70
- package/templates/firebase/lib/components/kasy_web_header.dart +4 -3
- package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
- package/templates/firebase/lib/core/config/features.dart +18 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +21 -0
- package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +1 -1
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +46 -124
- package/templates/firebase/lib/core/theme/icon_sizes.dart +47 -0
- package/templates/firebase/lib/core/theme/shadows.dart +13 -0
- package/templates/firebase/lib/core/theme/texts.dart +32 -0
- package/templates/firebase/lib/core/theme/theme.dart +2 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -0
- package/templates/firebase/lib/core/web_viewport_scale.dart +23 -4
- package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -7
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -1
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +61 -0
- package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +1 -1
- package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -1
- package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +3 -1
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +57 -29
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +47 -25
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +1 -1
- package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +1 -1
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +1 -1
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +2 -3
- package/templates/firebase/lib/features/home/home_components_page.dart +7 -1
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +54 -3
- package/templates/firebase/lib/features/home/home_image_grid.dart +1 -1
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +165 -209
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
- package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
- package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -6
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +6 -1
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +104 -156
- package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +1 -1
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +1 -1
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +3 -3
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +3 -2
- package/templates/firebase/lib/features/settings/settings_page.dart +264 -307
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +17 -8
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +4 -4
- package/templates/firebase/lib/features/settings/ui/components/edit_name_sheet.dart +115 -0
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +2 -2
- package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +13 -5
- package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
- package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +7 -1
- package/templates/firebase/lib/features/subscriptions/providers/premium_page_provider.dart +11 -3
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +2 -2
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +3 -3
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +1 -1
- package/templates/firebase/lib/i18n/en.i18n.json +13 -4
- package/templates/firebase/lib/i18n/es.i18n.json +13 -4
- package/templates/firebase/lib/i18n/pt.i18n.json +13 -4
- package/templates/firebase/lib/router.dart +2 -0
- package/templates/firebase/pubspec.yaml +1 -2
- package/templates/firebase/tool/design_check.dart +152 -0
- package/templates/firebase/web/stripe_success.html +64 -26
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
- package/templates/firebase/assets/images/review.png +0 -0
- package/templates/firebase/assets/images/update.png +0 -0
- package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +0 -19
- package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
- package/templates/firebase/login-redesign-preview.png +0 -0
|
@@ -1098,13 +1098,12 @@ class _ToolsTab extends ConsumerWidget {
|
|
|
1098
1098
|
_ActionCard(
|
|
1099
1099
|
icon: KasyIcons.note,
|
|
1100
1100
|
title: admin.update_bottom_sheet,
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
},
|
|
1101
|
+
// Preview the sheet over the current screen; dismissing (tap-outside /
|
|
1102
|
+
// Continue) just closes it and returns here, without navigating away.
|
|
1103
|
+
onTap: () => showUpdateBottomSheet(
|
|
1104
|
+
context: navigatorKey.currentContext!,
|
|
1105
|
+
version: '0.0.0',
|
|
1106
|
+
),
|
|
1108
1107
|
),
|
|
1109
1108
|
_ActionCard(
|
|
1110
1109
|
icon: KasyIcons.payment,
|
|
@@ -1148,6 +1147,10 @@ class _ToolsTab extends ConsumerWidget {
|
|
|
1148
1147
|
icon: KasyIcons.notification,
|
|
1149
1148
|
title: admin.copy_fcm_token,
|
|
1150
1149
|
onTap: () async {
|
|
1150
|
+
if (kIsWeb) {
|
|
1151
|
+
ref.read(toastProvider).alert(title: '', text: admin.native_only);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1151
1154
|
final token = await FirebaseMessaging.instance.getToken();
|
|
1152
1155
|
if (token == null) {
|
|
1153
1156
|
ref.read(toastProvider).alert(
|
|
@@ -1163,7 +1166,13 @@ class _ToolsTab extends ConsumerWidget {
|
|
|
1163
1166
|
_ActionCard(
|
|
1164
1167
|
icon: KasyIcons.notificationActive,
|
|
1165
1168
|
title: admin.ask_notification,
|
|
1166
|
-
onTap: ()
|
|
1169
|
+
onTap: () {
|
|
1170
|
+
if (kIsWeb) {
|
|
1171
|
+
ref.read(toastProvider).alert(title: '', text: admin.native_only);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
ref.read(notificationsSettingsProvider).askPermission();
|
|
1175
|
+
},
|
|
1167
1176
|
),
|
|
1168
1177
|
];
|
|
1169
1178
|
|
|
@@ -97,13 +97,13 @@ class _EditableUserAvatarState extends ConsumerState<EditableUserAvatar> {
|
|
|
97
97
|
child: Container(
|
|
98
98
|
width: 22,
|
|
99
99
|
height: 22,
|
|
100
|
-
decoration:
|
|
101
|
-
color:
|
|
100
|
+
decoration: BoxDecoration(
|
|
101
|
+
color: Colors.black.withValues(alpha: 0.6),
|
|
102
102
|
shape: BoxShape.circle,
|
|
103
103
|
),
|
|
104
104
|
child: const Icon(
|
|
105
105
|
KasyIcons.cameraAlt,
|
|
106
|
-
size:
|
|
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:
|
|
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:
|
|
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.
|
|
40
|
+
style: context.textTheme.titleSmall?.copyWith(
|
|
41
41
|
color: context.colors.onSurface,
|
|
42
42
|
),
|
|
43
43
|
),
|
package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart
CHANGED
|
@@ -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:
|
|
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 =
|
|
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(
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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;
|
|
@@ -312,7 +312,7 @@ class _ComparisonTable extends StatelessWidget {
|
|
|
312
312
|
isAvailable
|
|
313
313
|
? KasyIcons.check
|
|
314
314
|
: KasyIcons.close,
|
|
315
|
-
size:
|
|
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:
|
|
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:
|
|
96
|
+
size: KasyIconSize.xl,
|
|
97
97
|
),
|
|
98
98
|
const SizedBox(width: KasySpacing.smd),
|
|
99
99
|
Flexible(
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
436
|
+
child: Icon(icon, color: context.colors.background, size: KasyIconSize.sm),
|
|
437
437
|
),
|
|
438
438
|
)
|
|
439
439
|
: const SizedBox.shrink(),
|
|
@@ -402,8 +402,8 @@
|
|
|
402
402
|
"cancel_button": "Cancel"
|
|
403
403
|
},
|
|
404
404
|
"review_popup": {
|
|
405
|
-
"title": "Your
|
|
406
|
-
"description": "
|
|
405
|
+
"title": "Your feedback matters",
|
|
406
|
+
"description": "If the app has been helpful, a quick review goes a long way. Got ideas to improve it? We'd love to hear them.",
|
|
407
407
|
"cancel_button": "Suggest improvements",
|
|
408
408
|
"rate_button": "Write a review"
|
|
409
409
|
},
|
|
@@ -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)",
|
|
@@ -509,7 +517,7 @@
|
|
|
509
517
|
"error": "Something went wrong. Please try again."
|
|
510
518
|
},
|
|
511
519
|
"admin": {
|
|
512
|
-
"update_bottom_sheet": "
|
|
520
|
+
"update_bottom_sheet": "Preview what's new",
|
|
513
521
|
"paywalls": "Paywalls",
|
|
514
522
|
"test_onboarding": "Test onboarding",
|
|
515
523
|
"copy_user_id": "Copy user id",
|
|
@@ -518,12 +526,13 @@
|
|
|
518
526
|
"fcm_token_copied": "FCM Token copied to clipboard",
|
|
519
527
|
"fcm_token_unavailable": "Token unavailable (notifications disabled?)",
|
|
520
528
|
"ask_notification": "Ask for notification",
|
|
529
|
+
"native_only": "Available only in the native app (iOS / Android)",
|
|
521
530
|
"ask_review": "Ask for review",
|
|
522
531
|
"home_widgets_panel": "Home Widgets panel",
|
|
523
532
|
"home_widgets_title": "Home Widgets Panel",
|
|
524
533
|
"inspector_fab_title": "Widget inspector",
|
|
525
534
|
"inspector_fab_subtitle_prefix": "Global shortcut:",
|
|
526
|
-
"update_mywidget_title": "
|
|
535
|
+
"update_mywidget_title": "Update MyWidget Widget",
|
|
527
536
|
"update_mywidget_desc": "Call manual update for MyWidget widget",
|
|
528
537
|
"paywalls_title": "Paywalls Admin Panel",
|
|
529
538
|
"send_push_title": "Send notification",
|
|
@@ -402,8 +402,8 @@
|
|
|
402
402
|
"cancel_button": "Cancelar"
|
|
403
403
|
},
|
|
404
404
|
"review_popup": {
|
|
405
|
-
"title": "
|
|
406
|
-
"description": "
|
|
405
|
+
"title": "Tu opinión importa",
|
|
406
|
+
"description": "Si la app te ha sido útil, una reseña rápida marca la diferencia. ¿Tienes ideas para mejorarla? Nos encantaría escucharlas.",
|
|
407
407
|
"cancel_button": "Sugerir mejoras",
|
|
408
408
|
"rate_button": "Escribir una reseña"
|
|
409
409
|
},
|
|
@@ -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)",
|
|
@@ -509,7 +517,7 @@
|
|
|
509
517
|
"error": "Algo salió mal. Por favor, inténtalo de nuevo."
|
|
510
518
|
},
|
|
511
519
|
"admin": {
|
|
512
|
-
"update_bottom_sheet": "
|
|
520
|
+
"update_bottom_sheet": "Previsualizar novedades",
|
|
513
521
|
"paywalls": "Paywalls",
|
|
514
522
|
"test_onboarding": "Probar onboarding",
|
|
515
523
|
"copy_user_id": "Copiar ID de usuario",
|
|
@@ -518,12 +526,13 @@
|
|
|
518
526
|
"fcm_token_copied": "FCM Token copiado al portapapeles",
|
|
519
527
|
"fcm_token_unavailable": "Token no disponible (¿notificaciones desactivadas?)",
|
|
520
528
|
"ask_notification": "Pedir permiso de notificación",
|
|
529
|
+
"native_only": "Disponible solo en la app nativa (iOS / Android)",
|
|
521
530
|
"ask_review": "Pedir evaluación",
|
|
522
531
|
"home_widgets_panel": "Panel de Home Widgets",
|
|
523
532
|
"home_widgets_title": "Panel de Home Widgets",
|
|
524
533
|
"inspector_fab_title": "Inspector de widgets",
|
|
525
534
|
"inspector_fab_subtitle_prefix": "Atajo global:",
|
|
526
|
-
"update_mywidget_title": "
|
|
535
|
+
"update_mywidget_title": "Actualizar Widget MyWidget",
|
|
527
536
|
"update_mywidget_desc": "Llamar a la actualización manual para el widget MyWidget",
|
|
528
537
|
"paywalls_title": "Panel de Admin de Paywalls",
|
|
529
538
|
"send_push_title": "Enviar notificación",
|
|
@@ -402,8 +402,8 @@
|
|
|
402
402
|
"cancel_button": "Cancelar"
|
|
403
403
|
},
|
|
404
404
|
"review_popup": {
|
|
405
|
-
"title": "Sua opinião importa
|
|
406
|
-
"description": "
|
|
405
|
+
"title": "Sua opinião importa",
|
|
406
|
+
"description": "Se o app tem te ajudado, uma avaliação rápida faz toda a diferença. Prefere sugerir melhorias? A gente quer ouvir.",
|
|
407
407
|
"cancel_button": "Sugerir melhorias",
|
|
408
408
|
"rate_button": "Escrever uma avaliação"
|
|
409
409
|
},
|
|
@@ -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)",
|
|
@@ -509,7 +517,7 @@
|
|
|
509
517
|
"error": "Algo deu errado. Por favor, tente novamente."
|
|
510
518
|
},
|
|
511
519
|
"admin": {
|
|
512
|
-
"update_bottom_sheet": "
|
|
520
|
+
"update_bottom_sheet": "Pré-visualizar novidades",
|
|
513
521
|
"paywalls": "Paywalls",
|
|
514
522
|
"test_onboarding": "Testar onboarding",
|
|
515
523
|
"copy_user_id": "Copiar ID do usuário",
|
|
@@ -518,12 +526,13 @@
|
|
|
518
526
|
"fcm_token_copied": "FCM Token copiado para a área de transferência",
|
|
519
527
|
"fcm_token_unavailable": "Token não disponível (notificações desativadas?)",
|
|
520
528
|
"ask_notification": "Pedir permissão de notificação",
|
|
529
|
+
"native_only": "Disponível apenas no app nativo (iOS / Android)",
|
|
521
530
|
"ask_review": "Pedir avaliação",
|
|
522
531
|
"home_widgets_panel": "Painel de Home Widgets",
|
|
523
532
|
"home_widgets_title": "Painel de Home Widgets",
|
|
524
533
|
"inspector_fab_title": "Inspector de widgets",
|
|
525
534
|
"inspector_fab_subtitle_prefix": "Atalho global:",
|
|
526
|
-
"update_mywidget_title": "
|
|
535
|
+
"update_mywidget_title": "Atualizar Widget MyWidget",
|
|
527
536
|
"update_mywidget_desc": "Chamar atualização manual para o widget MyWidget",
|
|
528
537
|
"paywalls_title": "Painel Admin de Paywalls",
|
|
529
538
|
"send_push_title": "Enviar notificação",
|
|
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|
|
3
3
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
4
4
|
import 'package:go_router/go_router.dart';
|
|
5
5
|
import 'package:kasy_kit/core/bottom_menu/bottom_menu.dart';
|
|
6
|
+
import 'package:kasy_kit/core/chrome/chrome_visibility.dart';
|
|
6
7
|
import 'package:kasy_kit/core/config/features.dart';
|
|
7
8
|
import 'package:kasy_kit/core/data/api/analytics_api.dart';
|
|
8
9
|
import 'package:kasy_kit/core/guards/user_info_guard.dart';
|
|
@@ -62,6 +63,7 @@ GoRouter generateRouter({
|
|
|
62
63
|
},
|
|
63
64
|
observers: [
|
|
64
65
|
AnalyticsObserver(analyticsApi: MixpanelAnalyticsApi.instance()),
|
|
66
|
+
KasyChromeVisibilityObserver(),
|
|
65
67
|
|
|
66
68
|
...?observers,
|
|
67
69
|
],
|
|
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|
|
16
16
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
|
17
17
|
# In Windows, build-name is used as the major, minor, and patch parts
|
|
18
18
|
# of the product and file versions while build-number is used as the build suffix.
|
|
19
|
-
version: 1.0.0+
|
|
19
|
+
version: 1.0.0+39
|
|
20
20
|
|
|
21
21
|
environment:
|
|
22
22
|
sdk: ^3.11.0
|
|
@@ -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
|