kasy-cli 1.31.13 → 1.31.14
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/scaffold/backends/supabase/deploy.js +5 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +57 -17
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +5 -0
- package/package.json +1 -1
- package/templates/firebase/lib/components/kasy_sidebar.dart +60 -9
- 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/features/notifications/providers/unread_notifications_count_provider.dart +17 -0
- package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +13 -6
- package/templates/firebase/lib/i18n/en.i18n.json +2 -1
- package/templates/firebase/lib/i18n/es.i18n.json +2 -1
- package/templates/firebase/lib/i18n/pt.i18n.json +2 -1
|
@@ -363,6 +363,11 @@ async function enableGoogleSignIn(projectRef, webClientId, clientSecret) {
|
|
|
363
363
|
external_google_client_id: webClientId,
|
|
364
364
|
external_google_secret: clientSecret,
|
|
365
365
|
external_google_skip_nonce_check: true,
|
|
366
|
+
// Allow the web app's dev origin as a redirect target so the web OAuth flow
|
|
367
|
+
// (signInWithOAuth redirectTo) and email links (recovery/confirm) land back on
|
|
368
|
+
// the app. http://localhost:5555 is the port `kasy run --web` uses. Add your
|
|
369
|
+
// production web origin here when you deploy to the web.
|
|
370
|
+
uri_allow_list: 'http://localhost:5555,http://localhost:5555/**',
|
|
366
371
|
});
|
|
367
372
|
if (!result.ok) return { ok: false, error: result.error };
|
|
368
373
|
if (result.data.external_google_enabled === true) return { ok: true };
|
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';
|
|
@@ -212,16 +213,16 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
|
|
|
212
213
|
@override
|
|
213
214
|
Future<Credentials> signinWithGoogle() async {
|
|
214
215
|
if (kIsWeb) {
|
|
215
|
-
// google_sign_in'
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
await client.auth.
|
|
221
|
-
OAuthProvider.google,
|
|
222
|
-
|
|
216
|
+
// google_sign_in v7 can't do imperative auth on web. Get the Google ID token
|
|
217
|
+
// via Firebase's popup (zero manual config — reuses the Firebase web OAuth
|
|
218
|
+
// client + authorized domains, which the kit already sets up) and sign into
|
|
219
|
+
// Supabase with it. Supabase remains the auth backend.
|
|
220
|
+
final idToken = await _googleIdTokenFromWebPopup();
|
|
221
|
+
final res = await client.auth.signInWithIdToken(
|
|
222
|
+
provider: OAuthProvider.google,
|
|
223
|
+
idToken: idToken,
|
|
223
224
|
);
|
|
224
|
-
return Credentials(id:
|
|
225
|
+
return Credentials(id: res.user!.id, token: res.session?.accessToken ?? '');
|
|
225
226
|
}
|
|
226
227
|
final googleSignIn = GoogleSignIn.instance;
|
|
227
228
|
// Web: clientId = Web Client ID (no serverClientId needed)
|
|
@@ -264,6 +265,33 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
|
|
|
264
265
|
|
|
265
266
|
}
|
|
266
267
|
|
|
268
|
+
/// Web only: obtains a Google ID token via Firebase's popup. google_sign_in v7
|
|
269
|
+
/// can't do imperative auth on the web, but the kit already initializes Firebase
|
|
270
|
+
/// (for FCM), so we reuse Firebase's working web OAuth client to get the token,
|
|
271
|
+
/// then sign into Supabase with it — no Supabase callback / Google console step
|
|
272
|
+
/// needed. The token's audience is the Firebase web client ID, which is the same
|
|
273
|
+
/// one configured as Supabase's Google provider, so signInWithIdToken accepts it.
|
|
274
|
+
Future<String> _googleIdTokenFromWebPopup() async {
|
|
275
|
+
try {
|
|
276
|
+
final cred = await fb_auth.FirebaseAuth.instance.signInWithPopup(
|
|
277
|
+
fb_auth.GoogleAuthProvider(),
|
|
278
|
+
);
|
|
279
|
+
final idToken = (cred.credential as fb_auth.OAuthCredential?)?.idToken;
|
|
280
|
+
if (idToken == null) {
|
|
281
|
+
throw ApiError(code: 401, message: 'No Google ID token from Firebase popup.');
|
|
282
|
+
}
|
|
283
|
+
return idToken;
|
|
284
|
+
} on fb_auth.FirebaseAuthException catch (e) {
|
|
285
|
+
if (e.code == 'popup-closed-by-user' ||
|
|
286
|
+
e.code == 'cancelled-popup-request' ||
|
|
287
|
+
e.code == 'web-context-cancelled' ||
|
|
288
|
+
e.code == 'user-cancelled') {
|
|
289
|
+
throw const UserCancelledSignInException();
|
|
290
|
+
}
|
|
291
|
+
rethrow;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
267
295
|
@override
|
|
268
296
|
Future<Credentials> signinWithGooglePlay() {
|
|
269
297
|
// Google Play Games sign-in is not supported for Supabase backend.
|
|
@@ -323,14 +351,26 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
|
|
|
323
351
|
@override
|
|
324
352
|
Future<Credentials> signupFromAnonymousWithGoogle() async {
|
|
325
353
|
if (kIsWeb) {
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
354
|
+
// Web: get the Google ID token via Firebase's popup (see signinWithGoogle) and
|
|
355
|
+
// link it to the current anonymous Supabase user.
|
|
356
|
+
final idToken = await _googleIdTokenFromWebPopup();
|
|
357
|
+
try {
|
|
358
|
+
final response = await client.auth.linkIdentityWithIdToken(
|
|
359
|
+
provider: OAuthProvider.google,
|
|
360
|
+
idToken: idToken,
|
|
361
|
+
);
|
|
362
|
+
return Credentials(id: response.user!.id, token: response.session?.accessToken ?? '');
|
|
363
|
+
} on AuthException catch (e) {
|
|
364
|
+
if (e.code == 'identity_already_exists') {
|
|
365
|
+
await _deleteCurrentAnonymousUser();
|
|
366
|
+
final res = await client.auth.signInWithIdToken(
|
|
367
|
+
provider: OAuthProvider.google,
|
|
368
|
+
idToken: idToken,
|
|
369
|
+
);
|
|
370
|
+
return Credentials(id: res.user!.id, token: res.session?.accessToken ?? '');
|
|
371
|
+
}
|
|
372
|
+
rethrow;
|
|
373
|
+
}
|
|
334
374
|
}
|
|
335
375
|
final scopes = ['email'];
|
|
336
376
|
final googleSignIn = GoogleSignIn.instance;
|
|
@@ -26,6 +26,11 @@ dependencies:
|
|
|
26
26
|
facebook_app_events: ^0.24.0
|
|
27
27
|
firebase_app_installations: ^0.4.0+7
|
|
28
28
|
firebase_core: ^4.5.0
|
|
29
|
+
# Web-only Google sign-in: on web, google_sign_in v7 can't do imperative auth, so we
|
|
30
|
+
# get the Google ID token via Firebase's popup (zero manual config — reuses the
|
|
31
|
+
# Firebase web client + authorized domains) and hand it to Supabase signInWithIdToken.
|
|
32
|
+
# Supabase stays the auth backend; mobile keeps the native google_sign_in flow.
|
|
33
|
+
firebase_auth: ^6.1.4
|
|
29
34
|
firebase_messaging: ^16.1.2
|
|
30
35
|
firebase_remote_config: ^6.2.0
|
|
31
36
|
flutter:
|
package/package.json
CHANGED
|
@@ -213,8 +213,14 @@ class KasySidebar extends StatefulWidget {
|
|
|
213
213
|
this.profileAvatar,
|
|
214
214
|
this.profileGradient = KasyAvatarGradients.indigo,
|
|
215
215
|
this.onProfileTap,
|
|
216
|
+
this.notificationsUnread = 0,
|
|
216
217
|
});
|
|
217
218
|
|
|
219
|
+
/// Unread notification count. When greater than zero, the Notifications nav
|
|
220
|
+
/// item shows an unread dot (mirrors the bottom-bar badge). Purely an unread
|
|
221
|
+
/// indicator — not tied to push (which is native-only).
|
|
222
|
+
final int notificationsUnread;
|
|
223
|
+
|
|
218
224
|
final VoidCallback? onSettingsTap;
|
|
219
225
|
|
|
220
226
|
/// Whether the profile block is shown at the bottom of the rail. Set false to
|
|
@@ -496,6 +502,9 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
496
502
|
: (widget.routes![i].label ?? ''),
|
|
497
503
|
isActive: _activeItemId.isEmpty && currentIndex == i,
|
|
498
504
|
onTap: () => _navigateTo(i),
|
|
505
|
+
showBadge: i < meta.length &&
|
|
506
|
+
meta[i].icon == KasyIcons.notification &&
|
|
507
|
+
widget.notificationsUnread > 0,
|
|
499
508
|
),
|
|
500
509
|
// Static showcase extras (incl. the Income submenu).
|
|
501
510
|
for (final item in _kMainItems.skip(1))
|
|
@@ -842,6 +851,37 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
842
851
|
|
|
843
852
|
// ── Generic row (expanded) / icon+tooltip (collapsed) ────────────────────────
|
|
844
853
|
|
|
854
|
+
/// Overlays a small unread dot on the top-right of [child] when [show] is true.
|
|
855
|
+
/// The dot carries a thin border in the sidebar background color so it reads
|
|
856
|
+
/// cleanly over the icon.
|
|
857
|
+
Widget _withBadgeDot({
|
|
858
|
+
required Widget child,
|
|
859
|
+
required bool show,
|
|
860
|
+
required Color dotColor,
|
|
861
|
+
required Color borderColor,
|
|
862
|
+
}) {
|
|
863
|
+
if (!show) return child;
|
|
864
|
+
return Stack(
|
|
865
|
+
clipBehavior: Clip.none,
|
|
866
|
+
children: [
|
|
867
|
+
child,
|
|
868
|
+
Positioned(
|
|
869
|
+
top: -2,
|
|
870
|
+
right: -2,
|
|
871
|
+
child: Container(
|
|
872
|
+
width: 8,
|
|
873
|
+
height: 8,
|
|
874
|
+
decoration: BoxDecoration(
|
|
875
|
+
color: dotColor,
|
|
876
|
+
shape: BoxShape.circle,
|
|
877
|
+
border: Border.all(color: borderColor, width: 1.5),
|
|
878
|
+
),
|
|
879
|
+
),
|
|
880
|
+
),
|
|
881
|
+
],
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
845
885
|
Widget _buildItemRow(
|
|
846
886
|
_SidebarColors c, {
|
|
847
887
|
required IconData icon,
|
|
@@ -849,6 +889,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
849
889
|
required bool isActive,
|
|
850
890
|
required VoidCallback onTap,
|
|
851
891
|
bool isLogout = false,
|
|
892
|
+
bool showBadge = false,
|
|
852
893
|
List<Widget> trailing = const [],
|
|
853
894
|
double bottomGap = _kItemGap,
|
|
854
895
|
}) {
|
|
@@ -861,14 +902,19 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
861
902
|
if (_collapsed) {
|
|
862
903
|
return Padding(
|
|
863
904
|
padding: EdgeInsets.only(bottom: bottomGap),
|
|
864
|
-
child:
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
905
|
+
child: _withBadgeDot(
|
|
906
|
+
show: showBadge,
|
|
907
|
+
dotColor: c.logout,
|
|
908
|
+
borderColor: c.bg,
|
|
909
|
+
child: _ProTooltipIcon(
|
|
910
|
+
icon: icon,
|
|
911
|
+
label: label,
|
|
912
|
+
iconBg: fill,
|
|
913
|
+
iconColor: iconColor,
|
|
914
|
+
activeBg: c.activeBg,
|
|
915
|
+
colors: c,
|
|
916
|
+
onTap: onTap,
|
|
917
|
+
),
|
|
872
918
|
),
|
|
873
919
|
);
|
|
874
920
|
}
|
|
@@ -894,7 +940,12 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
894
940
|
),
|
|
895
941
|
child: Row(
|
|
896
942
|
children: [
|
|
897
|
-
|
|
943
|
+
_withBadgeDot(
|
|
944
|
+
show: showBadge,
|
|
945
|
+
dotColor: c.logout,
|
|
946
|
+
borderColor: c.bg,
|
|
947
|
+
child: Icon(icon, size: _kIconSize, color: iconColor),
|
|
948
|
+
),
|
|
898
949
|
const SizedBox(width: _kIconGap),
|
|
899
950
|
Expanded(
|
|
900
951
|
child: Text(
|
|
@@ -14,6 +14,7 @@ import 'package:kasy_kit/core/states/logout_action.dart';
|
|
|
14
14
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
15
15
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
16
16
|
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
17
|
+
import 'package:kasy_kit/features/notifications/providers/unread_notifications_count_provider.dart';
|
|
17
18
|
import 'package:kasy_kit/features/settings/ui/widgets/kasy_user_avatar.dart';
|
|
18
19
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
19
20
|
|
|
@@ -64,6 +65,10 @@ class BottomMenu extends StatelessWidget {
|
|
|
64
65
|
builder: (context, ref, _) {
|
|
65
66
|
final User user =
|
|
66
67
|
ref.watch(userStateNotifierProvider).user;
|
|
68
|
+
final int unread = ref
|
|
69
|
+
.watch(unreadNotificationsCountProvider)
|
|
70
|
+
.value ??
|
|
71
|
+
0;
|
|
67
72
|
final (String name, String email) = switch (user) {
|
|
68
73
|
final AuthenticatedUserData u => (
|
|
69
74
|
(u.name?.isNotEmpty ?? false)
|
|
@@ -81,6 +86,7 @@ class BottomMenu extends StatelessWidget {
|
|
|
81
86
|
profileName: name,
|
|
82
87
|
profileEmail: email,
|
|
83
88
|
profileAvatar: const KasyUserAvatar(),
|
|
89
|
+
notificationsUnread: unread,
|
|
84
90
|
);
|
|
85
91
|
},
|
|
86
92
|
),
|
|
@@ -1,48 +1,27 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
-
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
4
3
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
5
|
-
import 'package:kasy_kit/features/notifications/
|
|
4
|
+
import 'package:kasy_kit/features/notifications/providers/unread_notifications_count_provider.dart';
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
/// Bottom-bar notifications icon with an unread-count badge.
|
|
7
|
+
///
|
|
8
|
+
/// The badge is purely an unread indicator (works on web too) — it is not tied
|
|
9
|
+
/// to push notifications, which are native-only. The count comes from the shared
|
|
10
|
+
/// [unreadNotificationsCountProvider], which re-subscribes per signed-in user.
|
|
11
|
+
class BottomItemNotification extends ConsumerWidget {
|
|
8
12
|
const BottomItemNotification({super.key});
|
|
9
13
|
|
|
10
14
|
@override
|
|
11
|
-
|
|
12
|
-
_BottomItemNotificationState();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
class _BottomItemNotificationState
|
|
16
|
-
extends ConsumerState<BottomItemNotification> {
|
|
17
|
-
Stream<int>? _count$;
|
|
18
|
-
|
|
19
|
-
@override
|
|
20
|
-
Widget build(BuildContext context) {
|
|
21
|
-
final String? userId =
|
|
22
|
-
ref.watch(userStateNotifierProvider).user.idOrNull;
|
|
15
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
23
16
|
const icon = Icon(KasyIcons.notification);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return StreamBuilder<int>(
|
|
34
|
-
key: const ValueKey('notification-count'),
|
|
35
|
-
stream: _count$,
|
|
36
|
-
builder: (context, snapshot) {
|
|
37
|
-
final count = snapshot.data ?? 0;
|
|
38
|
-
if (count == 0) return icon;
|
|
39
|
-
return Badge(
|
|
40
|
-
backgroundColor: context.colors.error,
|
|
41
|
-
textColor: context.colors.onPrimary,
|
|
42
|
-
label: Text(count > 99 ? '99+' : '$count'),
|
|
43
|
-
child: icon,
|
|
44
|
-
);
|
|
45
|
-
},
|
|
17
|
+
final int count =
|
|
18
|
+
ref.watch(unreadNotificationsCountProvider).value ?? 0;
|
|
19
|
+
if (count == 0) return icon;
|
|
20
|
+
return Badge(
|
|
21
|
+
backgroundColor: context.colors.error,
|
|
22
|
+
textColor: context.colors.onError,
|
|
23
|
+
label: Text(count > 99 ? '99+' : '$count'),
|
|
24
|
+
child: icon,
|
|
46
25
|
);
|
|
47
26
|
}
|
|
48
27
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
2
|
+
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
3
|
+
import 'package:kasy_kit/features/notifications/repositories/notifications_repository.dart';
|
|
4
|
+
|
|
5
|
+
/// Live count of the signed-in user's unread notifications. Emits 0 when there
|
|
6
|
+
/// is no user. Shared source for badging the notifications item everywhere it
|
|
7
|
+
/// appears in the navigation (bottom bar, sidebar).
|
|
8
|
+
///
|
|
9
|
+
/// Watches the user id, so it automatically re-subscribes to the right stream
|
|
10
|
+
/// when the account changes (avoids leaking the previous user's count).
|
|
11
|
+
final unreadNotificationsCountProvider = StreamProvider.autoDispose<int>((ref) {
|
|
12
|
+
final String? userId = ref.watch(userStateNotifierProvider).user.idOrNull;
|
|
13
|
+
if (userId == null) return Stream<int>.value(0);
|
|
14
|
+
return ref
|
|
15
|
+
.read(notificationRepositoryProvider)
|
|
16
|
+
.listenToUnreadNotificationsCount(userId);
|
|
17
|
+
});
|
|
@@ -14,12 +14,18 @@ class DeleteUserButton extends ConsumerWidget {
|
|
|
14
14
|
label: t.settings.delete_account.button,
|
|
15
15
|
variant: KasyButtonVariant.ghost,
|
|
16
16
|
foregroundColor: context.colors.muted,
|
|
17
|
-
onPressed: ()
|
|
17
|
+
onPressed: () {
|
|
18
|
+
// Only warn about losing the subscription when the user actually has an
|
|
19
|
+
// active one — otherwise the generic permanent-deletion warning.
|
|
20
|
+
final bool isSubscriber =
|
|
21
|
+
ref.read(userStateNotifierProvider).subscription?.isActive ?? false;
|
|
22
|
+
final account = t.settings.delete_account;
|
|
23
|
+
showKasyConfirmDialog(
|
|
18
24
|
context,
|
|
19
|
-
title:
|
|
20
|
-
message:
|
|
21
|
-
cancelLabel:
|
|
22
|
-
confirmLabel:
|
|
25
|
+
title: account.title,
|
|
26
|
+
message: isSubscriber ? account.content_subscriber : account.content,
|
|
27
|
+
cancelLabel: account.cancel,
|
|
28
|
+
confirmLabel: account.confirm,
|
|
23
29
|
destructive: true,
|
|
24
30
|
onConfirmAsync: () async {
|
|
25
31
|
try {
|
|
@@ -34,7 +40,8 @@ class DeleteUserButton extends ConsumerWidget {
|
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
},
|
|
37
|
-
|
|
43
|
+
);
|
|
44
|
+
},
|
|
38
45
|
);
|
|
39
46
|
}
|
|
40
47
|
}
|
|
@@ -502,7 +502,8 @@
|
|
|
502
502
|
"delete_account": {
|
|
503
503
|
"button": "I want to delete my account",
|
|
504
504
|
"title": "Delete your account?",
|
|
505
|
-
"content": "Warning: this is permanent
|
|
505
|
+
"content": "Warning: this action is permanent and cannot be undone.",
|
|
506
|
+
"content_subscriber": "Warning: this is permanent. You will lose your active subscription, and creating a new account later (even with the same email) will not restore it.",
|
|
506
507
|
"cancel": "Cancel",
|
|
507
508
|
"confirm": "Yes, delete",
|
|
508
509
|
"error": "Something went wrong. Please try again."
|
|
@@ -502,7 +502,8 @@
|
|
|
502
502
|
"delete_account": {
|
|
503
503
|
"button": "Quiero eliminar mi cuenta",
|
|
504
504
|
"title": "¿Quieres eliminar tu cuenta?",
|
|
505
|
-
"content": "Advertencia: esta acción es permanente
|
|
505
|
+
"content": "Advertencia: esta acción es permanente y no se puede deshacer.",
|
|
506
|
+
"content_subscriber": "Advertencia: esta acción es permanente. Perderás tu suscripción activa, y crear una cuenta nueva más tarde (incluso con el mismo correo) no la recuperará.",
|
|
506
507
|
"cancel": "Cancelar",
|
|
507
508
|
"confirm": "Sí, eliminar",
|
|
508
509
|
"error": "Algo salió mal. Por favor, inténtalo de nuevo."
|
|
@@ -502,7 +502,8 @@
|
|
|
502
502
|
"delete_account": {
|
|
503
503
|
"button": "Quero excluir minha conta",
|
|
504
504
|
"title": "Quer excluir sua conta?",
|
|
505
|
-
"content": "Atenção: esta ação é permanente
|
|
505
|
+
"content": "Atenção: esta ação é permanente e não pode ser desfeita.",
|
|
506
|
+
"content_subscriber": "Atenção: esta ação é permanente. Você perde sua assinatura ativa, e criar uma nova conta depois (mesmo com o mesmo e-mail) não recupera ela.",
|
|
506
507
|
"cancel": "Cancelar",
|
|
507
508
|
"confirm": "Sim, excluir",
|
|
508
509
|
"error": "Algo deu errado. Por favor, tente novamente."
|