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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Kasy design-system guard-rail.
|
|
2
|
+
//
|
|
3
|
+
// Fails (exit 1) when a FEATURE SCREEN reaches around the design system and
|
|
4
|
+
// hardcodes a value that should come from a token / component. The design
|
|
5
|
+
// primitives themselves (lib/components, lib/core) are exempt — that is where
|
|
6
|
+
// the system is built, so raw values are expected there.
|
|
7
|
+
//
|
|
8
|
+
// This is intentionally a plain script, not a hidden dependency: read it, tune
|
|
9
|
+
// the lists below, or delete it. It enforces the project's *defaults*; it never
|
|
10
|
+
// locks anyone in.
|
|
11
|
+
//
|
|
12
|
+
// Run it locally or in CI:
|
|
13
|
+
// dart run tool/design_check.dart
|
|
14
|
+
//
|
|
15
|
+
// Escape hatch for a deliberate one-off: put `// design-check: ignore` on the
|
|
16
|
+
// offending line (the reason should be obvious from a nearby comment).
|
|
17
|
+
//
|
|
18
|
+
// See DESIGN_SYSTEM.md for the tokens/roles to use instead.
|
|
19
|
+
|
|
20
|
+
import 'dart:io';
|
|
21
|
+
|
|
22
|
+
/// The guard-rail only scans FEATURE SCREENS ([_scanRoot]). The design system
|
|
23
|
+
/// itself (lib/components, lib/core) and the app bootstrap (lib/main.dart) are
|
|
24
|
+
/// the *implementation* of the system, so raw values are expected there and are
|
|
25
|
+
/// out of scope by construction.
|
|
26
|
+
const String _scanRoot = 'lib/features';
|
|
27
|
+
|
|
28
|
+
/// Path fragments that are exempt even inside the feature layer.
|
|
29
|
+
/// - admin: internal admin tooling (kept out of the product design pass).
|
|
30
|
+
/// - generated files: not authored by hand.
|
|
31
|
+
/// - showcase/mockups: deliberately render raw values to demo them.
|
|
32
|
+
/// - subscriptions: the paywall is mid-redesign; re-enable when it lands.
|
|
33
|
+
const List<String> _exemptPathFragments = <String>[
|
|
34
|
+
'/admin/',
|
|
35
|
+
'admin_card.dart',
|
|
36
|
+
'.g.dart',
|
|
37
|
+
'.freezed.dart',
|
|
38
|
+
'home_components_preview_registry.dart',
|
|
39
|
+
'home_components_preview_page.dart',
|
|
40
|
+
'home_components_page.dart',
|
|
41
|
+
'design_system_page.dart',
|
|
42
|
+
'onboarding_module_mockups.dart',
|
|
43
|
+
'/features/subscriptions/',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const String _ignoreMarker = '// design-check: ignore';
|
|
47
|
+
|
|
48
|
+
class _Rule {
|
|
49
|
+
const _Rule(this.id, this.pattern, this.message);
|
|
50
|
+
final String id;
|
|
51
|
+
final RegExp pattern;
|
|
52
|
+
final String message;
|
|
53
|
+
|
|
54
|
+
/// Optional secondary token that must ALSO be on the line for the rule to
|
|
55
|
+
/// fire (used to scope `size:` to Icon lines only).
|
|
56
|
+
bool matches(String line) => pattern.hasMatch(line);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
final List<_Rule> _rules = <_Rule>[
|
|
60
|
+
_Rule(
|
|
61
|
+
'raw-material',
|
|
62
|
+
RegExp(r'\b(ElevatedButton|OutlinedButton|TextButton|AlertDialog|SnackBar)\s*\(|(?<![A-Za-z])Card\s*\('),
|
|
63
|
+
'Use the Kasy component (KasyButton / showKasyConfirmDialog / showKasyToast '
|
|
64
|
+
'/ KasyCard) instead of the raw Material widget.',
|
|
65
|
+
),
|
|
66
|
+
_Rule(
|
|
67
|
+
'hardcoded-font-size',
|
|
68
|
+
RegExp(r'fontSize:\s*\d'),
|
|
69
|
+
'Use a typography role (context.textTheme.* / KasyTextTheme.*) instead of a '
|
|
70
|
+
'literal fontSize.',
|
|
71
|
+
),
|
|
72
|
+
_Rule(
|
|
73
|
+
'raw-color',
|
|
74
|
+
// `Color(0x...)` literals, or `Colors.<named>` — but NOT `KasyColors.` (the
|
|
75
|
+
// token class, hence the lookbehind) and NOT the transparent / white* /
|
|
76
|
+
// black* helpers, which are the legitimate way to build scrims & overlays.
|
|
77
|
+
RegExp(r'Color\(0x|(?<![A-Za-z])Colors\.(?!transparent\b)(?!white)(?!black)[A-Za-z]'),
|
|
78
|
+
'Use a colour token (context.colors.*) instead of a raw Color/Colors value.',
|
|
79
|
+
),
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
// The icon rule needs two signals on the same line, so it is handled separately.
|
|
83
|
+
final RegExp _iconCall = RegExp(r'\bIcon\s*\(');
|
|
84
|
+
final RegExp _numericSize = RegExp(r'size:\s*\d');
|
|
85
|
+
|
|
86
|
+
bool _isExempt(String path) =>
|
|
87
|
+
_exemptPathFragments.any((String frag) => path.contains(frag));
|
|
88
|
+
|
|
89
|
+
void main() {
|
|
90
|
+
final Directory libDir = Directory(_scanRoot);
|
|
91
|
+
if (!libDir.existsSync()) {
|
|
92
|
+
stderr.writeln('design_check: run me from the package root (no $_scanRoot here).');
|
|
93
|
+
exit(2);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
final List<String> violations = <String>[];
|
|
97
|
+
int scanned = 0;
|
|
98
|
+
|
|
99
|
+
for (final FileSystemEntity entity in libDir.listSync(recursive: true)) {
|
|
100
|
+
if (entity is! File || !entity.path.endsWith('.dart')) continue;
|
|
101
|
+
final String path = entity.path.replaceAll(r'\', '/');
|
|
102
|
+
if (_isExempt('/$path')) continue;
|
|
103
|
+
scanned++;
|
|
104
|
+
|
|
105
|
+
final List<String> lines = entity.readAsLinesSync();
|
|
106
|
+
for (int i = 0; i < lines.length; i++) {
|
|
107
|
+
final String line = lines[i];
|
|
108
|
+
if (line.contains(_ignoreMarker)) continue;
|
|
109
|
+
final String code = _stripComment(line);
|
|
110
|
+
if (code.trim().isEmpty) continue;
|
|
111
|
+
|
|
112
|
+
for (final _Rule rule in _rules) {
|
|
113
|
+
if (rule.matches(code)) {
|
|
114
|
+
violations.add(_format(path, i + 1, rule.id, rule.message, line));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (_iconCall.hasMatch(code) && _numericSize.hasMatch(code)) {
|
|
118
|
+
violations.add(_format(
|
|
119
|
+
path,
|
|
120
|
+
i + 1,
|
|
121
|
+
'hardcoded-icon-size',
|
|
122
|
+
'Use a KasyIconSize.* token instead of a literal Icon size.',
|
|
123
|
+
line,
|
|
124
|
+
));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (violations.isEmpty) {
|
|
130
|
+
stdout.writeln('design_check: OK ($scanned feature files clean).');
|
|
131
|
+
exit(0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
stdout.writeln('design_check: ${violations.length} violation(s) found.\n');
|
|
135
|
+
stdout.writeln(violations.join('\n\n'));
|
|
136
|
+
stdout.writeln(
|
|
137
|
+
'\nFix by using a token/role/component (see DESIGN_SYSTEM.md), or mark a '
|
|
138
|
+
'deliberate exception with `$_ignoreMarker` on the line.',
|
|
139
|
+
);
|
|
140
|
+
exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Drops an end-of-line `//` comment so rules don't match commented-out code or
|
|
144
|
+
/// doc text (keeps string literals intact enough for these coarse checks).
|
|
145
|
+
String _stripComment(String line) {
|
|
146
|
+
final int idx = line.indexOf('//');
|
|
147
|
+
return idx == -1 ? line : line.substring(0, idx);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
String _format(String path, int line, String id, String message, String src) {
|
|
151
|
+
return ' $path:$line [$id]\n ${src.trim()}\n → $message';
|
|
152
|
+
}
|
|
@@ -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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
--
|
|
22
|
-
--
|
|
31
|
+
--accent-fg: #FFFFFF;
|
|
32
|
+
--success: #17C964;
|
|
33
|
+
--bg: #F5F5F5;
|
|
23
34
|
--card: #FFFFFF;
|
|
24
|
-
--text: #
|
|
25
|
-
--muted: #
|
|
26
|
-
--border: rgba(
|
|
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: #
|
|
31
|
-
--card: #
|
|
32
|
-
--text: #
|
|
33
|
-
--muted: #
|
|
34
|
-
--border: rgba(255, 255, 255, 0.
|
|
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",
|
|
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:
|
|
69
|
+
border-radius: 24px;
|
|
55
70
|
padding: 40px 32px;
|
|
56
71
|
text-align: center;
|
|
57
|
-
box-shadow:
|
|
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(
|
|
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:
|
|
70
|
-
h1 {
|
|
71
|
-
|
|
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:
|
|
101
|
+
border-radius: 14px;
|
|
76
102
|
padding: 14px 20px;
|
|
77
|
-
font-
|
|
103
|
+
font-family: inherit;
|
|
104
|
+
font-size: 16px;
|
|
78
105
|
font-weight: 600;
|
|
79
|
-
color:
|
|
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
|
-
|
|
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;
|
package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart
DELETED
|
@@ -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
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
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/api/analytics_api.dart';
|
|
5
|
-
import 'package:kasy_kit/core/data/api/tracking_api.dart';
|
|
6
|
-
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
7
|
-
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_background.dart';
|
|
8
|
-
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart';
|
|
9
|
-
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_module_mockups.dart';
|
|
10
|
-
import 'package:kasy_kit/features/subscriptions/repositories/subscription_repository.dart';
|
|
11
|
-
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
12
|
-
import 'package:permission_handler/permission_handler.dart';
|
|
13
|
-
|
|
14
|
-
/// ATT Permission Step
|
|
15
|
-
/// ATT is only available on iOS
|
|
16
|
-
/// This is the consent screen for iOS 14+ that asks the user to allow the app to access the ATT framework
|
|
17
|
-
/// In a few words you need this to get the IDFA (Identifier for Advertisers) which is used for tracking purposes
|
|
18
|
-
/// So you can create better facebook ads, google ads, etc...
|
|
19
|
-
class AttPermissionStep extends ConsumerWidget {
|
|
20
|
-
final String nextRoute;
|
|
21
|
-
|
|
22
|
-
const AttPermissionStep({
|
|
23
|
-
super.key,
|
|
24
|
-
required this.nextRoute,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
@override
|
|
28
|
-
Widget build(BuildContext context, WidgetRef ref) {
|
|
29
|
-
final translations = Translations.of(context).onboarding.att;
|
|
30
|
-
|
|
31
|
-
return OnboardingBackground(
|
|
32
|
-
child: OnboardingIllustrationScaffold(
|
|
33
|
-
step: 6,
|
|
34
|
-
title: translations.title,
|
|
35
|
-
description: translations.description,
|
|
36
|
-
image: const TrackingPermissionMockup(),
|
|
37
|
-
footerActions: [
|
|
38
|
-
KasyButton(
|
|
39
|
-
label: translations.continue_button,
|
|
40
|
-
expand: true,
|
|
41
|
-
onPressed: () async {
|
|
42
|
-
final Map<Permission, PermissionStatus> permission = await [
|
|
43
|
-
Permission.appTrackingTransparency,
|
|
44
|
-
].request();
|
|
45
|
-
final isGranted =
|
|
46
|
-
permission.values.first == PermissionStatus.granted;
|
|
47
|
-
ref.read(analyticsApiProvider).logEvent('att_request', {
|
|
48
|
-
'granted': isGranted,
|
|
49
|
-
});
|
|
50
|
-
final userId =
|
|
51
|
-
ref.read(userStateNotifierProvider).user.idOrNull;
|
|
52
|
-
if (userId != null) {
|
|
53
|
-
ref
|
|
54
|
-
.read(subscriptionRepositoryProvider)
|
|
55
|
-
.initUser(userId)
|
|
56
|
-
.ignore();
|
|
57
|
-
ref.read(facebookEventApiProvider).initUser(userId).ignore();
|
|
58
|
-
}
|
|
59
|
-
if (!context.mounted) return;
|
|
60
|
-
Navigator.of(context).pushNamed(nextRoute);
|
|
61
|
-
},
|
|
62
|
-
),
|
|
63
|
-
],
|
|
64
|
-
),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import 'package:flutter/material.dart';
|
|
2
|
-
import 'package:flutter_animate/flutter_animate.dart';
|
|
3
|
-
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
4
|
-
import 'package:kasy_kit/components/components.dart';
|
|
5
|
-
import 'package:kasy_kit/core/animations/movefade_anim.dart';
|
|
6
|
-
import 'package:kasy_kit/core/theme/theme.dart';
|
|
7
|
-
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
8
|
-
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_background.dart';
|
|
9
|
-
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_progress.dart';
|
|
10
|
-
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_step_header.dart';
|
|
11
|
-
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_sticky_footer.dart';
|
|
12
|
-
import 'package:kasy_kit/features/onboarding/ui/widgets/selectable_row_tile.dart';
|
|
13
|
-
|
|
14
|
-
typedef OptionBuilder = Widget Function(String key, bool selected);
|
|
15
|
-
|
|
16
|
-
typedef ReassuranceBuilder = Widget? Function(String key);
|
|
17
|
-
|
|
18
|
-
typedef OnOptionIdSelected = void Function(String id);
|
|
19
|
-
|
|
20
|
-
typedef OnValidate = void Function(String? key);
|
|
21
|
-
|
|
22
|
-
/// Single choice question with selectable tiles, matching the clean‑premium
|
|
23
|
-
/// onboarding layout (shared header, left‑aligned title, sticky footer).
|
|
24
|
-
class OnboardingRadioQuestion extends ConsumerStatefulWidget {
|
|
25
|
-
final int step;
|
|
26
|
-
final int totalSteps;
|
|
27
|
-
final String title;
|
|
28
|
-
final String description;
|
|
29
|
-
final String btnText;
|
|
30
|
-
final List<String> optionIds;
|
|
31
|
-
final OptionBuilder optionBuilder;
|
|
32
|
-
final ReassuranceBuilder? reassuranceBuilder;
|
|
33
|
-
final OnOptionIdSelected? onOptionIdSelected;
|
|
34
|
-
final OnValidate? onValidate;
|
|
35
|
-
|
|
36
|
-
const OnboardingRadioQuestion({
|
|
37
|
-
super.key,
|
|
38
|
-
required this.title,
|
|
39
|
-
required this.description,
|
|
40
|
-
required this.btnText,
|
|
41
|
-
required this.optionIds,
|
|
42
|
-
required this.optionBuilder,
|
|
43
|
-
required this.step,
|
|
44
|
-
this.totalSteps = kOnboardingSteps,
|
|
45
|
-
this.onOptionIdSelected,
|
|
46
|
-
this.onValidate,
|
|
47
|
-
this.reassuranceBuilder,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
@override
|
|
51
|
-
ConsumerState<OnboardingRadioQuestion> createState() =>
|
|
52
|
-
_OnboardingRadioQuestionState();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
class _OnboardingRadioQuestionState
|
|
56
|
-
extends ConsumerState<OnboardingRadioQuestion> {
|
|
57
|
-
String? selectedChoiceId;
|
|
58
|
-
|
|
59
|
-
@override
|
|
60
|
-
Widget build(BuildContext context) {
|
|
61
|
-
const gutter = KasySpacing.lg;
|
|
62
|
-
|
|
63
|
-
final scrollBody = Column(
|
|
64
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
65
|
-
children: [
|
|
66
|
-
OnboardingStepHeader(step: widget.step, totalSteps: widget.totalSteps),
|
|
67
|
-
Padding(
|
|
68
|
-
padding: const EdgeInsets.fromLTRB(gutter, KasySpacing.lg, gutter, 0),
|
|
69
|
-
child: MoveFadeAnim(
|
|
70
|
-
delayInMs: 120,
|
|
71
|
-
child: Text(
|
|
72
|
-
widget.title,
|
|
73
|
-
textAlign: TextAlign.start,
|
|
74
|
-
style: context.textTheme.headlineMedium?.copyWith(
|
|
75
|
-
color: context.colors.onBackground,
|
|
76
|
-
fontSize: 26,
|
|
77
|
-
fontWeight: FontWeight.w700,
|
|
78
|
-
letterSpacing: -0.2,
|
|
79
|
-
height: 1.2,
|
|
80
|
-
),
|
|
81
|
-
),
|
|
82
|
-
),
|
|
83
|
-
),
|
|
84
|
-
Padding(
|
|
85
|
-
padding: const EdgeInsets.fromLTRB(
|
|
86
|
-
gutter,
|
|
87
|
-
KasySpacing.smd,
|
|
88
|
-
gutter,
|
|
89
|
-
KasySpacing.lg,
|
|
90
|
-
),
|
|
91
|
-
child: MoveFadeAnim(
|
|
92
|
-
delayInMs: 200,
|
|
93
|
-
child: Text(
|
|
94
|
-
widget.description,
|
|
95
|
-
textAlign: TextAlign.start,
|
|
96
|
-
style: context.textTheme.bodyLarge?.copyWith(
|
|
97
|
-
color: context.colors.muted,
|
|
98
|
-
height: 1.45,
|
|
99
|
-
),
|
|
100
|
-
),
|
|
101
|
-
),
|
|
102
|
-
),
|
|
103
|
-
Padding(
|
|
104
|
-
padding: const EdgeInsets.symmetric(horizontal: gutter),
|
|
105
|
-
child: OnboardingSelectableRowGroup(
|
|
106
|
-
physics: const NeverScrollableScrollPhysics(),
|
|
107
|
-
options: widget.optionIds.map(
|
|
108
|
-
(e) {
|
|
109
|
-
final index = widget.optionIds.indexOf(e);
|
|
110
|
-
return Animate(
|
|
111
|
-
effects: [
|
|
112
|
-
FadeEffect(
|
|
113
|
-
delay: Duration(milliseconds: 280 + index * 80),
|
|
114
|
-
duration: const Duration(milliseconds: 450),
|
|
115
|
-
curve: Curves.easeOut,
|
|
116
|
-
),
|
|
117
|
-
MoveEffect(
|
|
118
|
-
delay: Duration(milliseconds: 280 + index * 80),
|
|
119
|
-
duration: const Duration(milliseconds: 450),
|
|
120
|
-
curve: Curves.easeOut,
|
|
121
|
-
begin: const Offset(0, 24),
|
|
122
|
-
end: Offset.zero,
|
|
123
|
-
),
|
|
124
|
-
],
|
|
125
|
-
child: widget.optionBuilder(e, e == selectedChoiceId),
|
|
126
|
-
);
|
|
127
|
-
},
|
|
128
|
-
).toList(),
|
|
129
|
-
onSelect: (index, selected) {
|
|
130
|
-
widget.onOptionIdSelected?.call(widget.optionIds[index]);
|
|
131
|
-
setState(() {
|
|
132
|
-
selectedChoiceId = widget.optionIds[index];
|
|
133
|
-
});
|
|
134
|
-
},
|
|
135
|
-
onSelectInfoWidget: widget.optionIds
|
|
136
|
-
.map((el) => widget.reassuranceBuilder?.call(el))
|
|
137
|
-
.toList(),
|
|
138
|
-
),
|
|
139
|
-
),
|
|
140
|
-
const SizedBox(height: KasySpacing.xl),
|
|
141
|
-
],
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
return OnboardingBackground(
|
|
145
|
-
child: Column(
|
|
146
|
-
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
147
|
-
children: [
|
|
148
|
-
Expanded(
|
|
149
|
-
child: SafeArea(
|
|
150
|
-
bottom: false,
|
|
151
|
-
child: SingleChildScrollView(
|
|
152
|
-
padding: const EdgeInsets.only(bottom: KasySpacing.md),
|
|
153
|
-
child: ResponsiveBuilder(
|
|
154
|
-
small: scrollBody,
|
|
155
|
-
medium: Center(
|
|
156
|
-
child: ConstrainedBox(
|
|
157
|
-
constraints: const BoxConstraints(maxWidth: 600),
|
|
158
|
-
child: scrollBody,
|
|
159
|
-
),
|
|
160
|
-
),
|
|
161
|
-
),
|
|
162
|
-
),
|
|
163
|
-
),
|
|
164
|
-
),
|
|
165
|
-
DeviceSizeBuilder(
|
|
166
|
-
builder: (device) => OnboardingStickyFooter(
|
|
167
|
-
maxContentWidth: device == DeviceType.small ? null : 600,
|
|
168
|
-
children: [
|
|
169
|
-
KasyButton(
|
|
170
|
-
label: widget.btnText,
|
|
171
|
-
expand: true,
|
|
172
|
-
onPressed: selectedChoiceId == null
|
|
173
|
-
? null
|
|
174
|
-
: () => widget.onValidate?.call(selectedChoiceId),
|
|
175
|
-
),
|
|
176
|
-
],
|
|
177
|
-
),
|
|
178
|
-
),
|
|
179
|
-
],
|
|
180
|
-
),
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
@@ -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
|
-
}
|