kasy-cli 1.31.13 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/new.js +15 -1
- package/lib/scaffold/CHANGELOG.json +9 -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 +22 -0
- package/lib/scaffold/backends/supabase/deploy.js +5 -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 +69 -17
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +6 -0
- package/lib/scaffold/generate.js +1 -1
- package/lib/scaffold/shared/generator-utils.js +22 -3
- package/lib/utils/i18n/messages-en.js +2 -0
- package/lib/utils/i18n/messages-es.js +2 -0
- package/lib/utils/i18n/messages-pt.js +2 -0
- package/package.json +2 -2
- 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/kasy_accordion.dart +2 -2
- package/templates/firebase/lib/components/kasy_alert.dart +1 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +3 -3
- 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_sidebar.dart +62 -11
- 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 +1 -1
- package/templates/firebase/lib/components/kasy_web_header.dart +4 -3
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +6 -0
- package/templates/firebase/lib/core/bottom_menu/notification_bottom_item.dart +16 -37
- package/templates/firebase/lib/core/config/features.dart +13 -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 +1 -1
- 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 +1 -1
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -1
- 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 +36 -14
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +27 -11
- 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 +1 -1
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +22 -3
- package/templates/firebase/lib/features/home/home_image_grid.dart +1 -1
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +2 -2
- package/templates/firebase/lib/features/notifications/providers/unread_notifications_count_provider.dart +17 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +6 -1
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +35 -38
- 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_module_mockups.dart +3 -3
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +1 -1
- package/templates/firebase/lib/features/settings/settings_page.dart +264 -307
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
- package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +13 -6
- 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 +10 -1
- package/templates/firebase/lib/i18n/es.i18n.json +10 -1
- package/templates/firebase/lib/i18n/pt.i18n.json +10 -1
- package/templates/firebase/pubspec.yaml +0 -1
- 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/lib/features/authentication/ui/components/facebook_signin.dart +0 -19
- package/templates/firebase/login-redesign-preview.png +0 -0
package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import 'dart:convert';
|
|
2
2
|
import 'package:crypto/crypto.dart';
|
|
3
|
+
import 'package:firebase_auth/firebase_auth.dart' as fb_auth;
|
|
3
4
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
4
5
|
import 'package:flutter/services.dart' show PlatformException;
|
|
5
6
|
import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
|
|
@@ -149,6 +150,13 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
|
|
|
149
150
|
|
|
150
151
|
@override
|
|
151
152
|
Future<Credentials> signinWithApple() async {
|
|
153
|
+
// Apple on web needs a paid Apple Service ID + secret (not configured here);
|
|
154
|
+
// getAppleIDCredential force-unwraps webAuthenticationOptions and crashes on
|
|
155
|
+
// web. The UI hides the Apple button on web; guard here too so a programmatic
|
|
156
|
+
// call fails with a clear error instead of a null-check crash.
|
|
157
|
+
if (kIsWeb) {
|
|
158
|
+
throw ApiError(code: 501, message: 'Apple sign-in on web is not supported.');
|
|
159
|
+
}
|
|
152
160
|
final rawNonce = client.auth.generateRawNonce();
|
|
153
161
|
final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString();
|
|
154
162
|
|
|
@@ -212,16 +220,16 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
|
|
|
212
220
|
@override
|
|
213
221
|
Future<Credentials> signinWithGoogle() async {
|
|
214
222
|
if (kIsWeb) {
|
|
215
|
-
// google_sign_in'
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
await client.auth.
|
|
221
|
-
OAuthProvider.google,
|
|
222
|
-
|
|
223
|
+
// google_sign_in v7 can't do imperative auth on web. Get the Google ID token
|
|
224
|
+
// via Firebase's popup (zero manual config — reuses the Firebase web OAuth
|
|
225
|
+
// client + authorized domains, which the kit already sets up) and sign into
|
|
226
|
+
// Supabase with it. Supabase remains the auth backend.
|
|
227
|
+
final idToken = await _googleIdTokenFromWebPopup();
|
|
228
|
+
final res = await client.auth.signInWithIdToken(
|
|
229
|
+
provider: OAuthProvider.google,
|
|
230
|
+
idToken: idToken,
|
|
223
231
|
);
|
|
224
|
-
return Credentials(id:
|
|
232
|
+
return Credentials(id: res.user!.id, token: res.session?.accessToken ?? '');
|
|
225
233
|
}
|
|
226
234
|
final googleSignIn = GoogleSignIn.instance;
|
|
227
235
|
// Web: clientId = Web Client ID (no serverClientId needed)
|
|
@@ -264,6 +272,33 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
|
|
|
264
272
|
|
|
265
273
|
}
|
|
266
274
|
|
|
275
|
+
/// Web only: obtains a Google ID token via Firebase's popup. google_sign_in v7
|
|
276
|
+
/// can't do imperative auth on the web, but the kit already initializes Firebase
|
|
277
|
+
/// (for FCM), so we reuse Firebase's working web OAuth client to get the token,
|
|
278
|
+
/// then sign into Supabase with it — no Supabase callback / Google console step
|
|
279
|
+
/// needed. The token's audience is the Firebase web client ID, which is the same
|
|
280
|
+
/// one configured as Supabase's Google provider, so signInWithIdToken accepts it.
|
|
281
|
+
Future<String> _googleIdTokenFromWebPopup() async {
|
|
282
|
+
try {
|
|
283
|
+
final cred = await fb_auth.FirebaseAuth.instance.signInWithPopup(
|
|
284
|
+
fb_auth.GoogleAuthProvider(),
|
|
285
|
+
);
|
|
286
|
+
final idToken = (cred.credential as fb_auth.OAuthCredential?)?.idToken;
|
|
287
|
+
if (idToken == null) {
|
|
288
|
+
throw ApiError(code: 401, message: 'No Google ID token from Firebase popup.');
|
|
289
|
+
}
|
|
290
|
+
return idToken;
|
|
291
|
+
} on fb_auth.FirebaseAuthException catch (e) {
|
|
292
|
+
if (e.code == 'popup-closed-by-user' ||
|
|
293
|
+
e.code == 'cancelled-popup-request' ||
|
|
294
|
+
e.code == 'web-context-cancelled' ||
|
|
295
|
+
e.code == 'user-cancelled') {
|
|
296
|
+
throw const UserCancelledSignInException();
|
|
297
|
+
}
|
|
298
|
+
rethrow;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
267
302
|
@override
|
|
268
303
|
Future<Credentials> signinWithGooglePlay() {
|
|
269
304
|
// Google Play Games sign-in is not supported for Supabase backend.
|
|
@@ -281,6 +316,11 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
|
|
|
281
316
|
|
|
282
317
|
@override
|
|
283
318
|
Future<Credentials> signupFromAnonymousWithApple() async {
|
|
319
|
+
// See signinWithApple: Apple on web is unsupported here; the UI hides the button
|
|
320
|
+
// on web, and this guard prevents a null-check crash on a programmatic call.
|
|
321
|
+
if (kIsWeb) {
|
|
322
|
+
throw ApiError(code: 501, message: 'Apple sign-in on web is not supported.');
|
|
323
|
+
}
|
|
284
324
|
final rawNonce = client.auth.generateRawNonce();
|
|
285
325
|
final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString();
|
|
286
326
|
|
|
@@ -323,14 +363,26 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
|
|
|
323
363
|
@override
|
|
324
364
|
Future<Credentials> signupFromAnonymousWithGoogle() async {
|
|
325
365
|
if (kIsWeb) {
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
366
|
+
// Web: get the Google ID token via Firebase's popup (see signinWithGoogle) and
|
|
367
|
+
// link it to the current anonymous Supabase user.
|
|
368
|
+
final idToken = await _googleIdTokenFromWebPopup();
|
|
369
|
+
try {
|
|
370
|
+
final response = await client.auth.linkIdentityWithIdToken(
|
|
371
|
+
provider: OAuthProvider.google,
|
|
372
|
+
idToken: idToken,
|
|
373
|
+
);
|
|
374
|
+
return Credentials(id: response.user!.id, token: response.session?.accessToken ?? '');
|
|
375
|
+
} on AuthException catch (e) {
|
|
376
|
+
if (e.code == 'identity_already_exists') {
|
|
377
|
+
await _deleteCurrentAnonymousUser();
|
|
378
|
+
final res = await client.auth.signInWithIdToken(
|
|
379
|
+
provider: OAuthProvider.google,
|
|
380
|
+
idToken: idToken,
|
|
381
|
+
);
|
|
382
|
+
return Credentials(id: res.user!.id, token: res.session?.accessToken ?? '');
|
|
383
|
+
}
|
|
384
|
+
rethrow;
|
|
385
|
+
}
|
|
334
386
|
}
|
|
335
387
|
final scopes = ['email'];
|
|
336
388
|
final googleSignIn = GoogleSignIn.instance;
|
package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart
CHANGED
|
@@ -30,6 +30,7 @@ class StripeBackendApi {
|
|
|
30
30
|
String? successUrl,
|
|
31
31
|
String? cancelUrl,
|
|
32
32
|
String? locale,
|
|
33
|
+
bool? allowPromoCodes,
|
|
33
34
|
}) async {
|
|
34
35
|
final res = await _client.functions.invoke(
|
|
35
36
|
'stripe-create-checkout-session',
|
|
@@ -38,16 +39,25 @@ class StripeBackendApi {
|
|
|
38
39
|
if (successUrl != null) 'successUrl': successUrl,
|
|
39
40
|
if (cancelUrl != null) 'cancelUrl': cancelUrl,
|
|
40
41
|
if (locale != null) 'locale': locale,
|
|
42
|
+
if (allowPromoCodes != null) 'allowPromoCodes': allowPromoCodes,
|
|
41
43
|
},
|
|
42
44
|
);
|
|
43
45
|
return (res.data as Map)['url'] as String;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/// Create a Customer Portal session (manage / cancel) and return its URL.
|
|
47
|
-
|
|
49
|
+
/// Pass [planSwitching] = true to auto-configure the portal with
|
|
50
|
+
/// upgrade/downgrade support (no manual Stripe dashboard setup needed).
|
|
51
|
+
Future<String> createPortalSession({
|
|
52
|
+
String? returnUrl,
|
|
53
|
+
bool? planSwitching,
|
|
54
|
+
}) async {
|
|
48
55
|
final res = await _client.functions.invoke(
|
|
49
56
|
'stripe-create-portal-session',
|
|
50
|
-
body: {
|
|
57
|
+
body: {
|
|
58
|
+
if (returnUrl != null) 'returnUrl': returnUrl,
|
|
59
|
+
if (planSwitching != null) 'planSwitching': planSwitching,
|
|
60
|
+
},
|
|
51
61
|
);
|
|
52
62
|
return (res.data as Map)['url'] as String;
|
|
53
63
|
}
|
|
@@ -25,6 +25,12 @@ dependencies:
|
|
|
25
25
|
dio: ^5.9.2
|
|
26
26
|
facebook_app_events: ^0.24.0
|
|
27
27
|
firebase_app_installations: ^0.4.0+7
|
|
28
|
+
# Web-only Google sign-in: on web, google_sign_in v7 can't do imperative auth, so we
|
|
29
|
+
# get the Google ID token via Firebase's popup (zero manual config, reuses the
|
|
30
|
+
# Firebase web client + authorized domains) and hand it to Supabase signInWithIdToken.
|
|
31
|
+
# Supabase stays the auth backend; mobile keeps the native google_sign_in flow.
|
|
32
|
+
# Kept before firebase_core so the deps stay alphabetical (sort_pub_dependencies).
|
|
33
|
+
firebase_auth: ^6.1.4
|
|
28
34
|
firebase_core: ^4.5.0
|
|
29
35
|
firebase_messaging: ^16.1.2
|
|
30
36
|
firebase_remote_config: ^6.2.0
|
package/lib/scaffold/generate.js
CHANGED
|
@@ -219,7 +219,7 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
|
|
|
219
219
|
await writeVsCodeLaunch(targetDir, appName, backend, modules, answers, language);
|
|
220
220
|
await writeEnvExample(targetDir, modules, answers, language);
|
|
221
221
|
await writeEnvFileIfMissing(targetDir);
|
|
222
|
-
await writeFeaturesConfig(targetDir, modules, answers, language);
|
|
222
|
+
await writeFeaturesConfig(targetDir, modules, answers, language, backend);
|
|
223
223
|
await writeRouter(targetDir, modules, packageName, moduleAnswers.defaultPaywall || 'basic');
|
|
224
224
|
await writeMakefile(targetDir, language, backend, modules, answers);
|
|
225
225
|
await writeKitSetup(targetDir, {
|
|
@@ -616,7 +616,7 @@ async function writeRouter(projectDir, modules, packageName, defaultPaywall = 'b
|
|
|
616
616
|
* @param {object} [answers={}] - moduleAnswers (rcTestKey, stripe keys, etc.)
|
|
617
617
|
* @param {string} [language='en'] - User's CLI language (en, pt, es)
|
|
618
618
|
*/
|
|
619
|
-
async function writeFeaturesConfig(projectDir, modules, answers = {}, language = 'en') {
|
|
619
|
+
async function writeFeaturesConfig(projectDir, modules, answers = {}, language = 'en', backend = 'firebase') {
|
|
620
620
|
const withOnboarding = modules.includes('onboarding');
|
|
621
621
|
const withAiChat = modules.includes('ai_chat');
|
|
622
622
|
const withFeedback = modules.includes('feedback');
|
|
@@ -624,6 +624,10 @@ async function writeFeaturesConfig(projectDir, modules, answers = {}, language =
|
|
|
624
624
|
const withStripe = modules.includes('stripe');
|
|
625
625
|
const withLocalReminders = modules.includes('local_reminders');
|
|
626
626
|
const withWeb = modules.includes('web');
|
|
627
|
+
// Apple sign-in on web only works on Firebase (signInWithPopup). Supabase/API
|
|
628
|
+
// throw on web (no Service ID + token exchange), so they must ship this false
|
|
629
|
+
// and hide the Apple button on web. Native always shows it.
|
|
630
|
+
const withAppleWebSignin = backend === 'firebase';
|
|
627
631
|
|
|
628
632
|
const f = getStrings(language).features;
|
|
629
633
|
const content = `${f.comment1}
|
|
@@ -635,7 +639,14 @@ const bool withFeedback = ${withFeedback};
|
|
|
635
639
|
const bool withRevenuecat = ${withRevenuecat};
|
|
636
640
|
// Stripe web subscriptions module (independent from RevenueCat mobile IAP).
|
|
637
641
|
const bool withStripe = ${withStripe};
|
|
642
|
+
// When true, Stripe Checkout shows a promo-code / coupon field.
|
|
643
|
+
const bool withStripePromoCodes = true;
|
|
644
|
+
// When true, the Stripe Customer Portal lets subscribers switch plans (upgrade / downgrade).
|
|
645
|
+
const bool withStripePlanSwitching = true;
|
|
638
646
|
const bool withLocalReminders = ${withLocalReminders};
|
|
647
|
+
// Apple sign-in on web: Firebase supports it (signInWithPopup); Supabase/API throw
|
|
648
|
+
// on web, so they ship false. Native always shows the Apple button.
|
|
649
|
+
const bool withAppleWebSignin = ${withAppleWebSignin};
|
|
639
650
|
${f.comment3}
|
|
640
651
|
${f.comment4}
|
|
641
652
|
${f.comment5}
|
|
@@ -1347,10 +1358,18 @@ async function removeFacebookSigninFromAuthPages(projectDir) {
|
|
|
1347
1358
|
for (const p of pages) {
|
|
1348
1359
|
if (!(await fs.pathExists(p))) continue;
|
|
1349
1360
|
let content = await fs.readFile(p, 'utf8');
|
|
1350
|
-
//
|
|
1361
|
+
// Legacy: remove the standalone FacebookSigninComponent + its import, if present.
|
|
1351
1362
|
content = content.replace(/^import 'package:[^']+\/features\/authentication\/ui\/components\/facebook_signin\.dart';\n/m, '');
|
|
1352
|
-
// Remove component line (any leading whitespace)
|
|
1353
1363
|
content = content.replace(/[ \t]*const FacebookSigninComponent\(\),\n/g, '');
|
|
1364
|
+
// Current UI: the Facebook button is an inline _SocialSigninTile/_SocialSignupTile
|
|
1365
|
+
// in the social row (label: t.auth.signin.facebook ... signinWithFacebook()). Strip
|
|
1366
|
+
// the whole tile plus the SizedBox spacer that precedes it, anchored on the facebook
|
|
1367
|
+
// label so the Google/Apple tiles are never touched. Runs before dartFix/format, so
|
|
1368
|
+
// it matches the kit's raw formatting verbatim.
|
|
1369
|
+
content = content.replace(
|
|
1370
|
+
/\n[ \t]*const SizedBox\(width: KasySpacing\.sm\),\n[ \t]*Expanded\(\n[ \t]*child: _Social(?:Signin|Signup)Tile\(\n[ \t]*label: t\.auth\.signin\.facebook,[\s\S]*?\.signinWithFacebook\(\),\n[ \t]*\),\n[ \t]*\),/,
|
|
1371
|
+
'',
|
|
1372
|
+
);
|
|
1354
1373
|
await fs.writeFile(p, content, 'utf8');
|
|
1355
1374
|
}
|
|
1356
1375
|
}
|
|
@@ -740,6 +740,7 @@ module.exports = {
|
|
|
740
740
|
'new.success.featuresInstalled': 'Features enabled:',
|
|
741
741
|
'new.success.bundleId': 'App identifier (bundle ID)',
|
|
742
742
|
'new.success.bundleId.hint': "Your app's unique identifier on Android, iOS and Firebase (push).",
|
|
743
|
+
'new.success.api.serverContracts': 'API backend: you must implement the server contracts (delete account, AI chat and push). See patch/README.md',
|
|
743
744
|
'new.success.nextSteps': 'Next steps:',
|
|
744
745
|
'new.success.step.cd': 'Go to your project folder:',
|
|
745
746
|
'new.success.step.deploy': 'Push the server to Firebase (DB + functions):',
|
|
@@ -754,6 +755,7 @@ module.exports = {
|
|
|
754
755
|
'new.google.manualHint': 'Google Sign-In: enable manually in the Console (Google provider):',
|
|
755
756
|
'new.google.manualHint.noEmail': 'Google Sign-In: could not detect a support email (gcloud has no account). Enable manually in the Console:',
|
|
756
757
|
'new.google.supabaseManual': 'Google Sign-In: client created, but the secret was not available yet. Enable it later in the Supabase dashboard (Authentication > Providers > Google).',
|
|
758
|
+
'new.google.localhostDomainWarn': 'Google sign-in on web: could not authorize localhost in the Firebase authorized domains automatically. If the Google popup fails with "unauthorized-domain", add localhost in Firebase Console > Authentication > Settings > Authorized domains.',
|
|
757
759
|
'new.fcm.ok': 'generated automatically',
|
|
758
760
|
'new.fcm.failSupabase': 'not generated (GCP permission still propagating); set FIREBASE_SERVICE_ACCOUNT_JSON in your Supabase secrets',
|
|
759
761
|
'new.fcm.failApi': 'not generated (GCP permission still propagating); run the command again in a few minutes',
|
|
@@ -740,6 +740,7 @@ module.exports = {
|
|
|
740
740
|
'new.success.featuresInstalled': 'Recursos activados:',
|
|
741
741
|
'new.success.bundleId': 'Identificador de la app (bundle ID)',
|
|
742
742
|
'new.success.bundleId.hint': 'Identificador único de tu app en Android, iOS y Firebase (push).',
|
|
743
|
+
'new.success.api.serverContracts': 'Backend API: debes implementar los contratos del servidor (eliminar cuenta, IA chat y push). Consulta patch/README.md',
|
|
743
744
|
'new.success.nextSteps': 'Próximos pasos:',
|
|
744
745
|
'new.success.step.cd': 'Ve a la carpeta del proyecto:',
|
|
745
746
|
'new.success.step.deploy': 'Sube el servidor a Firebase (DB + funciones):',
|
|
@@ -754,6 +755,7 @@ module.exports = {
|
|
|
754
755
|
'new.google.manualHint': 'Inicio de sesión con Google: actívalo manualmente en la consola (proveedor Google):',
|
|
755
756
|
'new.google.manualHint.noEmail': 'Inicio de sesión con Google: no detecté un email de soporte (gcloud sin cuenta). Actívalo manualmente en la consola:',
|
|
756
757
|
'new.google.supabaseManual': 'Inicio de sesión con Google: cliente creado, pero el secret aún no estaba disponible. Actívalo luego en el panel de Supabase (Authentication > Providers > Google).',
|
|
758
|
+
'new.google.localhostDomainWarn': 'Inicio de sesión con Google en web: no se pudo autorizar localhost en los dominios de Firebase automáticamente. Si el popup de Google falla con "unauthorized-domain", agrega localhost en Firebase Console > Authentication > Settings > Authorized domains.',
|
|
757
759
|
'new.fcm.ok': 'generada automáticamente',
|
|
758
760
|
'new.fcm.failSupabase': 'no generada (permiso de GCP aún propagando); define FIREBASE_SERVICE_ACCOUNT_JSON en los secrets de Supabase',
|
|
759
761
|
'new.fcm.failApi': 'no generada (permiso de GCP aún propagando); ejecuta el comando de nuevo en unos minutos',
|
|
@@ -740,6 +740,7 @@ module.exports = {
|
|
|
740
740
|
'new.success.featuresInstalled': 'Recursos ativados:',
|
|
741
741
|
'new.success.bundleId': 'Identificador do app (bundle ID)',
|
|
742
742
|
'new.success.bundleId.hint': 'Identificador único do seu app no Android, iOS e Firebase (push).',
|
|
743
|
+
'new.success.api.serverContracts': 'Backend API: você precisa implementar os contratos do servidor (excluir conta, IA chat e push). Veja patch/README.md',
|
|
743
744
|
'new.success.nextSteps': 'Próximos passos:',
|
|
744
745
|
'new.success.step.cd': 'Entre na pasta do projeto:',
|
|
745
746
|
'new.success.step.deploy': 'Suba o servidor pro Firebase (banco + funções):',
|
|
@@ -754,6 +755,7 @@ module.exports = {
|
|
|
754
755
|
'new.google.manualHint': 'Login com Google: ative manualmente no Console (provedor Google):',
|
|
755
756
|
'new.google.manualHint.noEmail': 'Login com Google: não consegui detectar um e-mail de suporte (gcloud sem conta). Ative manualmente no Console:',
|
|
756
757
|
'new.google.supabaseManual': 'Login com Google: client criado, mas o secret ainda não estava disponível. Ative depois no painel do Supabase (Authentication > Providers > Google).',
|
|
758
|
+
'new.google.localhostDomainWarn': 'Login com Google na web: não consegui autorizar o localhost nos domínios do Firebase automaticamente. Se o popup do Google falhar com "unauthorized-domain", adicione localhost em Firebase Console > Authentication > Settings > Authorized domains.',
|
|
757
759
|
'new.fcm.ok': 'gerada automaticamente',
|
|
758
760
|
'new.fcm.failSupabase': 'não gerada (permissão do GCP ainda propagando); defina FIREBASE_SERVICE_ACCOUNT_JSON nos secrets do Supabase',
|
|
759
761
|
'new.fcm.failApi': 'não gerada (permissão do GCP ainda propagando); rode o comando de novo em alguns minutos',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kasy-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.0",
|
|
4
4
|
"description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"kasy": "./bin/kasy.js"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
-
"prepack": "node scripts/check-feature-patches.js && node scripts/bundle-template.js && node test/generated-matches-kit.test.js && node test/supabase-verify-jwt.test.js && node test/supabase-cors.test.js && node test/supabase-google-web.test.js && node test/backend-pubspec-local-reminders.test.js",
|
|
34
|
+
"prepack": "node scripts/check-feature-patches.js && node scripts/bundle-template.js && node test/generated-matches-kit.test.js && node test/supabase-verify-jwt.test.js && node test/supabase-cors.test.js && node test/supabase-google-web.test.js && node test/backend-pubspec-local-reminders.test.js && node test/stripe-webhook-orphan-guard.test.js && node test/facebook-strip.test.js && node test/path-provider-pin.test.js && node test/features-flags-parity.test.js",
|
|
35
35
|
"start": "node ./bin/kasy.js",
|
|
36
36
|
"setup": "node ./bin/kasy.js setup",
|
|
37
37
|
"doctor": "node ./bin/kasy.js doctor",
|
|
@@ -69,7 +69,13 @@ Requires an [Apple Developer](https://developer.apple.com) account (paid).
|
|
|
69
69
|
1. Open `ios/Runner.xcworkspace` in Xcode
|
|
70
70
|
2. Target **Runner** → **Signing & Capabilities** → **+ Capability** → add **Sign In with Apple**
|
|
71
71
|
|
|
72
|
-
> **
|
|
72
|
+
> **iOS / macOS**: the Apple button shows automatically once the steps above are done.
|
|
73
|
+
>
|
|
74
|
+
> **Android**: the Apple button is hidden by design (it needs the paid Services ID web flow and adds little on Android for a SaaS). Leave it hidden.
|
|
75
|
+
>
|
|
76
|
+
> **Web (Firebase)**: works after Steps 1-4 above (`withAppleWebSignin` already ships `true`). The Services ID Return URL (`firebaseapp.com/__/auth/handler`) covers the popup flow.
|
|
77
|
+
>
|
|
78
|
+
> **Web (Supabase)**: the CLI ships `withAppleWebSignin = false` (native iOS works; web needs more). To enable it: in Supabase → Authentication → Providers → Apple, add the **client secret** (a JWT signed with your `.p8` key + Services ID), then set `withAppleWebSignin = true` in `lib/core/config/features.dart`.
|
|
73
79
|
|
|
74
80
|
---
|
|
75
81
|
|
|
@@ -69,7 +69,13 @@ Requiere cuenta de [Apple Developer](https://developer.apple.com) (de pago).
|
|
|
69
69
|
1. Abre `ios/Runner.xcworkspace` en Xcode
|
|
70
70
|
2. Target **Runner** → **Signing & Capabilities** → **+ Capability** → agrega **Sign In with Apple**
|
|
71
71
|
|
|
72
|
-
> **
|
|
72
|
+
> **iOS / macOS**: el botón Apple aparece automáticamente tras los pasos anteriores.
|
|
73
|
+
>
|
|
74
|
+
> **Android**: el botón Apple queda oculto por defecto (necesita el flujo del Services ID de pago y aporta poco en Android para un SaaS). Déjalo oculto.
|
|
75
|
+
>
|
|
76
|
+
> **Web (Firebase)**: funciona tras los Pasos 1 a 4 anteriores (`withAppleWebSignin` ya viene `true`). La Return URL del Services ID (`firebaseapp.com/__/auth/handler`) cubre el flujo de popup.
|
|
77
|
+
>
|
|
78
|
+
> **Web (Supabase)**: la CLI genera `withAppleWebSignin = false` (iOS nativo funciona; la web necesita más). Para habilitar: en Supabase → Authentication → Providers → Apple, agrega el **client secret** (un JWT firmado con la clave `.p8` + Services ID) y luego define `withAppleWebSignin = true` en `lib/core/config/features.dart`.
|
|
73
79
|
|
|
74
80
|
---
|
|
75
81
|
|
|
@@ -69,7 +69,13 @@ Requer conta [Apple Developer](https://developer.apple.com) (paga).
|
|
|
69
69
|
1. Abra `ios/Runner.xcworkspace` no Xcode
|
|
70
70
|
2. Target **Runner** → **Signing & Capabilities** → **+ Capability** → adicione **Sign In with Apple**
|
|
71
71
|
|
|
72
|
-
> **
|
|
72
|
+
> **iOS / macOS**: o botão Apple aparece automaticamente depois dos passos acima.
|
|
73
|
+
>
|
|
74
|
+
> **Android**: o botão Apple fica escondido por padrão (exige o fluxo do Services ID pago e agrega pouco no Android para um SaaS). Deixe escondido.
|
|
75
|
+
>
|
|
76
|
+
> **Web (Firebase)**: funciona depois dos Passos 1 a 4 acima (`withAppleWebSignin` já vem `true`). A Return URL do Services ID (`firebaseapp.com/__/auth/handler`) cobre o fluxo de popup.
|
|
77
|
+
>
|
|
78
|
+
> **Web (Supabase)**: a CLI gera `withAppleWebSignin = false` (iOS nativo funciona; web precisa de mais). Para habilitar: em Supabase → Authentication → Providers → Apple, adicione o **client secret** (um JWT assinado com a chave `.p8` + Services ID) e depois defina `withAppleWebSignin = true` em `lib/core/config/features.dart`.
|
|
73
79
|
|
|
74
80
|
---
|
|
75
81
|
|
|
@@ -125,6 +125,8 @@ export const createCheckoutSession = onCall(
|
|
|
125
125
|
const price = await stripe.prices.retrieve(priceId, {expand: ["product"]});
|
|
126
126
|
const trialDays = trialDaysFor(price, price.product as Stripe.Product);
|
|
127
127
|
|
|
128
|
+
const allowPromoCodes = request.data?.allowPromoCodes as boolean | undefined;
|
|
129
|
+
|
|
128
130
|
const session = await stripe.checkout.sessions.create({
|
|
129
131
|
mode: "subscription",
|
|
130
132
|
customer: customerId,
|
|
@@ -136,13 +138,70 @@ export const createCheckoutSession = onCall(
|
|
|
136
138
|
metadata: {firebaseUID: uid},
|
|
137
139
|
...(trialDays ? {trial_period_days: trialDays} : {}),
|
|
138
140
|
},
|
|
141
|
+
...(allowPromoCodes ? {allow_promotion_codes: true} : {}),
|
|
139
142
|
});
|
|
140
143
|
return {url: session.url};
|
|
141
144
|
},
|
|
142
145
|
);
|
|
143
146
|
|
|
144
147
|
// ---------------------------------------------------------------------------
|
|
145
|
-
//
|
|
148
|
+
// getOrCreatePortalConfig — creates (once) a Customer Portal configuration
|
|
149
|
+
// with subscription_update (plan switching) enabled and caches its ID in
|
|
150
|
+
// Firestore so we don't recreate it on every portal open. Falls back to
|
|
151
|
+
// undefined (default portal) if there are no prices to switch between.
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
async function getOrCreatePortalConfig(stripe: Stripe): Promise<string | undefined> {
|
|
154
|
+
const db = admin.firestore();
|
|
155
|
+
const configRef = db.doc("_config/stripe_portal");
|
|
156
|
+
const snap = await configRef.get();
|
|
157
|
+
const cachedId = snap.data()?.configId as string | undefined;
|
|
158
|
+
if (cachedId) {
|
|
159
|
+
try {
|
|
160
|
+
const cfg = await stripe.billingPortal.configurations.retrieve(cachedId);
|
|
161
|
+
if (cfg.active) return cachedId;
|
|
162
|
+
} catch {
|
|
163
|
+
// Cached config was deleted on Stripe — recreate below
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Build allowed-prices list grouped by product. STRIPE_PRODUCT_ID narrows the
|
|
168
|
+
// query to a single product; if it's not set we use all active recurring prices.
|
|
169
|
+
const productId = stripeProductId.value();
|
|
170
|
+
const priceParams: Stripe.PriceListParams = {active: true, type: "recurring", limit: 100};
|
|
171
|
+
if (productId) priceParams.product = productId;
|
|
172
|
+
const {data: prices} = await stripe.prices.list(priceParams);
|
|
173
|
+
|
|
174
|
+
if (prices.length === 0) return undefined;
|
|
175
|
+
|
|
176
|
+
const byProduct: Record<string, string[]> = {};
|
|
177
|
+
for (const p of prices) {
|
|
178
|
+
const pid = typeof p.product === "string" ? p.product : p.product.id;
|
|
179
|
+
if (!byProduct[pid]) byProduct[pid] = [];
|
|
180
|
+
byProduct[pid].push(p.id);
|
|
181
|
+
}
|
|
182
|
+
const products = Object.entries(byProduct).map(([prod, priceIds]) => ({
|
|
183
|
+
product: prod,
|
|
184
|
+
prices: priceIds,
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
const config = await stripe.billingPortal.configurations.create({
|
|
188
|
+
features: {
|
|
189
|
+
subscription_update: {
|
|
190
|
+
enabled: true,
|
|
191
|
+
default_allowed_updates: ["price"],
|
|
192
|
+
products,
|
|
193
|
+
},
|
|
194
|
+
subscription_cancel: {enabled: true, mode: "at_period_end"},
|
|
195
|
+
payment_method_update: {enabled: true},
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await configRef.set({configId: config.id}, {merge: true});
|
|
200
|
+
return config.id;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// createPortalSession — Stripe Customer Portal (manage / cancel / switch plan).
|
|
146
205
|
// ---------------------------------------------------------------------------
|
|
147
206
|
export const createPortalSession = onCall(
|
|
148
207
|
{secrets: [stripeSecretKey]},
|
|
@@ -150,6 +209,7 @@ export const createPortalSession = onCall(
|
|
|
150
209
|
const uid = request.auth?.uid;
|
|
151
210
|
if (!uid) throw new HttpsError("unauthenticated", "Sign in required");
|
|
152
211
|
const returnUrl = (request.data?.returnUrl as string | undefined) ?? "";
|
|
212
|
+
const planSwitching = request.data?.planSwitching as boolean | undefined;
|
|
153
213
|
|
|
154
214
|
const stripe = stripeClient();
|
|
155
215
|
const snap = await admin
|
|
@@ -161,9 +221,16 @@ export const createPortalSession = onCall(
|
|
|
161
221
|
if (!customerId) {
|
|
162
222
|
throw new HttpsError("failed-precondition", "No Stripe customer for user");
|
|
163
223
|
}
|
|
224
|
+
|
|
225
|
+
// When plan switching is requested, resolve (or create) a portal configuration
|
|
226
|
+
// that has subscription_update enabled. This removes the need for any manual
|
|
227
|
+
// setup in the Stripe dashboard.
|
|
228
|
+
const configId = planSwitching ? await getOrCreatePortalConfig(stripe) : undefined;
|
|
229
|
+
|
|
164
230
|
const session = await stripe.billingPortal.sessions.create({
|
|
165
231
|
customer: customerId,
|
|
166
232
|
return_url: returnUrl,
|
|
233
|
+
...(configId ? {configuration: configId} : {}),
|
|
167
234
|
});
|
|
168
235
|
return {url: session.url};
|
|
169
236
|
},
|
|
@@ -189,11 +256,18 @@ async function upsertFromStripeSubscription(sub: Stripe.Subscription): Promise<v
|
|
|
189
256
|
console.log("[stripe-webhook] subscription without firebaseUID metadata, skipping");
|
|
190
257
|
return;
|
|
191
258
|
}
|
|
259
|
+
// Skip if the user no longer exists. Deleting an account cancels the Stripe
|
|
260
|
+
// customer, which fires customer.subscription.deleted AFTER deleteUserAccount
|
|
261
|
+
// already removed subscriptions/{uid} — without this guard the webhook would
|
|
262
|
+
// re-create an orphan doc for a user that is gone. (The Supabase webhook does
|
|
263
|
+
// the same check.) The lookup also gives us the email to denormalize below.
|
|
264
|
+
const user = await usersRepository.getFromId(uid);
|
|
265
|
+
if (!user) {
|
|
266
|
+
console.log(`[stripe-webhook] user ${uid} not found (likely deleted), skipping`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
192
269
|
const now = Timestamp.now();
|
|
193
270
|
const existing = await subscriptionsRepository.getFromUserId(uid);
|
|
194
|
-
// Denormalize the subscriber's email onto the Firestore subscription doc (see
|
|
195
|
-
// SubscriptionData.email) so a subscribers list reads it without a second hop.
|
|
196
|
-
const user = await usersRepository.getFromId(uid);
|
|
197
271
|
// In Stripe API v18 the billing period lives on each subscription item.
|
|
198
272
|
const item = sub.items.data[0];
|
|
199
273
|
const priceId = item?.price?.id ?? "";
|
|
@@ -211,7 +285,7 @@ async function upsertFromStripeSubscription(sub: Stripe.Subscription): Promise<v
|
|
|
211
285
|
expirationDate: expiration,
|
|
212
286
|
store: Stores.STRIPE,
|
|
213
287
|
productId: priceId,
|
|
214
|
-
email: user
|
|
288
|
+
email: user.email,
|
|
215
289
|
},
|
|
216
290
|
subscriptionsRepository,
|
|
217
291
|
);
|
|
@@ -366,7 +366,7 @@ class _TrailingChevron extends StatelessWidget {
|
|
|
366
366
|
duration: const Duration(milliseconds: 200),
|
|
367
367
|
child: Icon(
|
|
368
368
|
KasyIcons.chevronDown,
|
|
369
|
-
size:
|
|
369
|
+
size: KasyIconSize.md,
|
|
370
370
|
color: context.colors.muted,
|
|
371
371
|
),
|
|
372
372
|
);
|
|
@@ -376,7 +376,7 @@ class _TrailingChevron extends StatelessWidget {
|
|
|
376
376
|
duration: const Duration(milliseconds: 200),
|
|
377
377
|
child: Icon(
|
|
378
378
|
KasyIcons.chevronRight,
|
|
379
|
-
size:
|
|
379
|
+
size: KasyIconSize.md,
|
|
380
380
|
color: context.colors.muted,
|
|
381
381
|
),
|
|
382
382
|
);
|
|
@@ -292,7 +292,7 @@ class KasyAppBar extends StatelessWidget {
|
|
|
292
292
|
),
|
|
293
293
|
_ => KasyChromeOrbIconButton(
|
|
294
294
|
icon: KasyIcons.arrowBackIos,
|
|
295
|
-
iconSize:
|
|
295
|
+
iconSize: KasyIconSize.md,
|
|
296
296
|
foregroundColor: orbFg,
|
|
297
297
|
fillColor: orbFill,
|
|
298
298
|
onPressed: handleBack,
|
|
@@ -404,7 +404,7 @@ class KasyAppBar extends StatelessWidget {
|
|
|
404
404
|
case KasyAppBarStyle.subpage:
|
|
405
405
|
return KasyChromeOrbIconButton(
|
|
406
406
|
icon: isDark ? KasyIcons.lightMode : KasyIcons.darkMode,
|
|
407
|
-
iconSize:
|
|
407
|
+
iconSize: KasyIconSize.lg,
|
|
408
408
|
foregroundColor: iconFg,
|
|
409
409
|
fillColor: orbFill,
|
|
410
410
|
onPressed: () {
|
|
@@ -421,7 +421,7 @@ class KasyAppBar extends StatelessWidget {
|
|
|
421
421
|
if (trailing != null) return trailing!;
|
|
422
422
|
return KasyChromeOrbIconButton(
|
|
423
423
|
icon: isDark ? KasyIcons.lightMode : KasyIcons.darkMode,
|
|
424
|
-
iconSize:
|
|
424
|
+
iconSize: KasyIconSize.lg,
|
|
425
425
|
foregroundColor: iconFg,
|
|
426
426
|
fillColor: orbFill,
|
|
427
427
|
onPressed: () {
|
|
@@ -298,7 +298,7 @@ class _IconBubble extends StatelessWidget {
|
|
|
298
298
|
width: 72,
|
|
299
299
|
height: 72,
|
|
300
300
|
decoration: BoxDecoration(color: p.background, shape: BoxShape.circle),
|
|
301
|
-
child: Icon(icon, size:
|
|
301
|
+
child: Icon(icon, size: KasyIconSize.xxl, color: p.foreground),
|
|
302
302
|
);
|
|
303
303
|
}
|
|
304
304
|
|
|
@@ -417,17 +417,17 @@ class KasyButton extends StatelessWidget {
|
|
|
417
417
|
iconOnlyExtent: 40,
|
|
418
418
|
horizontalPadding: EdgeInsets.symmetric(horizontal: 16),
|
|
419
419
|
labelFontSize: 13,
|
|
420
|
-
iconSize:
|
|
421
|
-
iconOnlyGlyphSize:
|
|
420
|
+
iconSize: KasyIconSize.sm,
|
|
421
|
+
iconOnlyGlyphSize: KasyIconSize.xxs,
|
|
422
422
|
loadingSpinnerExtent: 13,
|
|
423
423
|
),
|
|
424
424
|
KasyButtonSize.medium => const _KasyButtonMetrics(
|
|
425
|
-
height:
|
|
426
|
-
iconOnlyExtent:
|
|
425
|
+
height: 45,
|
|
426
|
+
iconOnlyExtent: 45,
|
|
427
427
|
horizontalPadding: EdgeInsets.symmetric(horizontal: 18),
|
|
428
428
|
labelFontSize: 14,
|
|
429
|
-
iconSize:
|
|
430
|
-
iconOnlyGlyphSize:
|
|
429
|
+
iconSize: KasyIconSize.md,
|
|
430
|
+
iconOnlyGlyphSize: KasyIconSize.xs,
|
|
431
431
|
loadingSpinnerExtent: 14,
|
|
432
432
|
),
|
|
433
433
|
KasyButtonSize.large => const _KasyButtonMetrics(
|
|
@@ -435,8 +435,8 @@ class KasyButton extends StatelessWidget {
|
|
|
435
435
|
iconOnlyExtent: 54,
|
|
436
436
|
horizontalPadding: EdgeInsets.symmetric(horizontal: 22),
|
|
437
437
|
labelFontSize: 15,
|
|
438
|
-
iconSize:
|
|
439
|
-
iconOnlyGlyphSize:
|
|
438
|
+
iconSize: KasyIconSize.md,
|
|
439
|
+
iconOnlyGlyphSize: KasyIconSize.md,
|
|
440
440
|
loadingSpinnerExtent: 15,
|
|
441
441
|
),
|
|
442
442
|
};
|
|
@@ -36,7 +36,7 @@ class KasyChip extends StatelessWidget {
|
|
|
36
36
|
final KasyColors c = context.colors;
|
|
37
37
|
final Widget? avatar = icon == null
|
|
38
38
|
? null
|
|
39
|
-
: Icon(icon, size:
|
|
39
|
+
: Icon(icon, size: KasyIconSize.md, color: enabled ? c.primary : c.muted);
|
|
40
40
|
final OutlinedBorder chipShape = RoundedRectangleBorder(
|
|
41
41
|
borderRadius: BorderRadius.circular(KasyRadius.full),
|
|
42
42
|
side: BorderSide(color: c.outline.withValues(alpha: 0.45)),
|