Skip to content

feat(ui_auth): show confirmation dialog when trying to unlink a provider #116

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/firebase_ui_auth/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ class FirebaseAuthUIExample extends StatelessWidget {
showMFATile: kIsWeb ||
platform == TargetPlatform.iOS ||
platform == TargetPlatform.android,
showUnlinkConfirmationDialog: true,
);
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Future<bool> showReauthenticateDialog({
final reauthenticated = await showGeneralDialog<bool>(
context: context,
barrierDismissible: true,
barrierLabel: l.cancelLabel,
barrierLabel: l.cancelButtonLabel,
pageBuilder: (_, __, ___) => FirebaseUIActions.inherit(
from: context,
child: ReauthenticateDialog(
Expand Down
36 changes: 36 additions & 0 deletions packages/firebase_ui_auth/lib/src/screens/profile_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,13 @@ class _LinkedProvidersRow extends StatefulWidget {
final FirebaseAuth? auth;
final List<AuthProvider> providers;
final VoidCallback onProviderUnlinked;
final bool showUnlinkConfirmationDialog;

const _LinkedProvidersRow({
this.auth,
required this.providers,
required this.onProviderUnlinked,
required this.showUnlinkConfirmationDialog,
});

@override
Expand All @@ -201,13 +203,41 @@ class _LinkedProvidersRowState extends State<_LinkedProvidersRow> {
});
}

void Function() pop(bool value) {
return () {
Navigator.of(context).pop(value);
};
}

Future<void> _unlinkProvider(BuildContext context, String providerId) async {
setState(() {
unlinkingProvider = providerId;
error = null;
});

bool? confirmed = !widget.showUnlinkConfirmationDialog;

if (!confirmed) {
final l = FirebaseUILocalizations.labelsOf(context);

confirmed = await showAdaptiveDialog<bool?>(
context: context,
builder: (context) {
return UniversalAlert(
onConfirm: pop(true),
onCancel: pop(false),
title: l.ulinkProviderAlertTitle,
confirmButtonText: l.confirmUnlinkButtonLabel,
cancelButtonText: l.cancelButtonLabel,
message: l.unlinkProviderAlertMessage,
);
},
);
}

try {
if (!(confirmed ?? false)) return;

final user = widget.auth!.currentUser!;
await user.unlink(providerId);
await user.reload();
Expand Down Expand Up @@ -712,6 +742,10 @@ class ProfileScreen extends MultiProviderScreen {
/// are ignored.
final Widget? avatar;

/// Indicates wether a confirmation dialog should be shown when the user
/// tries to unlink a provider.
final bool showUnlinkConfirmationDialog;

const ProfileScreen({
super.key,
super.auth,
Expand All @@ -726,6 +760,7 @@ class ProfileScreen extends MultiProviderScreen {
this.cupertinoNavigationBar,
this.actionCodeSettings,
this.showMFATile = false,
this.showUnlinkConfirmationDialog = false,
});

Future<bool> _reauthenticate(BuildContext context) {
Expand Down Expand Up @@ -819,6 +854,7 @@ class ProfileScreen extends MultiProviderScreen {
auth: auth,
providers: linkedProviders,
onProviderUnlinked: providersScopeKey.rebuild,
showUnlinkConfirmationDialog: showUnlinkConfirmationDialog,
),
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class DifferentMethodSignInDialog extends StatelessWidget {
onSignedIn: onSignedIn,
),
UniversalButton(
text: l.cancelLabel,
text: l.cancelButtonLabel,
onPressed: () => Navigator.of(context).pop(),
),
],
Expand Down
20 changes: 20 additions & 0 deletions packages/firebase_ui_localizations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,30 @@ and make sure that your custom delegate extends `LocalizationsDelegate<FirebaseU

## Contributing

### Adding a new language

If you want to add a new language, make sure to add a relevant `.arb` file into `lib/i10n`.

- copy `lib/i10n/firebase_ui_en.arb` to `lib/i10n/firebase_ui_<your-language-code>.arb`
- translate labels
- run `dart run firebase_ui_localizations:gen_l10n`
- commit the `.arb` and generated `.dart` file
- submit a PR

### Adding a new label to existing languages

If you want to add new labels to existing languages,

- Execute `dart run firebase_ui_localizations:add_label`:

```bash
dart run firebase_ui_localizations:add_label
Label name?: someNewLabel
Label description?: This will go to the doc comment of the label
English translation?: Some new label
Done!
```

- Execute `dart run firebase_ui_localizations:gen_l10n`
- Commit the changes
- Submit a PR
15 changes: 13 additions & 2 deletions packages/firebase_ui_localizations/bin/add_label.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,22 @@ String prompt(String tag) {

Future<void> main(List<String> args) async {
final name = prompt('Label name');
final description = prompt('Label description');
final englishTranslation = prompt('English translation');

final cwd = Directory.current.path;
final l10nSrc = Directory(path.join(cwd, 'lib', 'l10n'));

final enArb = File(path.join(l10nSrc.path, 'firebase_ui_en.arb'));
final enContent =
jsonDecode(await enArb.readAsString()) as Map<String, dynamic>;

if (enContent.containsKey(name)) {
stderr.writeln('Label "$name" already exists');
exit(1);
}

final description = prompt('Label description');
final englishTranslation = prompt('English translation');

final files = l10nSrc.listSync().whereType<File>().toList();
final futures = files.map((e) async {
final newContent = await addLabel(e, name, description, englishTranslation);
Expand All @@ -48,6 +58,7 @@ Future<Map<String, dynamic>> addLabel(
String englishTranslation,
) async {
final content = jsonDecode(await file.readAsString()) as Map<String, dynamic>;

return {
...content,
"@@last_modified": DateTime.now().toIso8601String(),
Expand Down
133 changes: 133 additions & 0 deletions packages/firebase_ui_localizations/bin/gen_l10n.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ void main() async {
}).expand((element) => element);

await Future.wait([...genOps.cast<Future>()]);

await generateDefaultLocalizations(
labelsByLocale['en']['default'].cast<String, dynamic>(),
licenseHeader,
);

await generateLanguagesList(labelsByLocale, licenseHeader);
Process.runSync('dart', ['format', outDir.path]);
}
Expand Down Expand Up @@ -226,3 +232,130 @@ Future<String> getLicenseHeader() async {
return '// $e';
}).join('\n');
}

Future<void> generateDefaultLocalizations(
Map<String, dynamic> arb,
String licenseHeader,
) async {
final labels = arb.entries.where(isLabelEntry).map((e) {
final meta = arb['@${e.key}'] ?? {};

return Label(
key: e.key,
translation: e.value,
description: meta['description'],
);
}).toList()
..sort((a, b) => a.key.compareTo(b.key));

final content = await getDefaultLocalizationsContent(labels, licenseHeader);
final outFile = File(path.join(outDir.path, 'default_localizations.dart'));

if (!outFile.existsSync()) {
outFile.createSync(recursive: true);
}

final out = outFile.openWrite();
out.write(content);
await out.flush();
await out.close();
}

Future<String> getDefaultLocalizationsContent(
List<Label> labels,
String licenseHeader,
) async {
final sb = StringBuffer();

sb.writeln(licenseHeader);
sb.writeln(defaultLocalizationsHeader);

sb.writeln('abstract class FirebaseUILocalizationLabels {');
sb.writeln(' const FirebaseUILocalizationLabels();');

for (var label in labels) {
sb.writeln();
if (label.description != null && label.description!.isNotEmpty) {
const prefix = ' /// ';
for (var line in breakIntoLines(label.description!, 80 - prefix.length)) {
sb.writeln('$prefix$line');
}
}
sb.writeln(' String get ${label.key};');
}

sb.writeln('}');
sb.writeln();

sb.writeln(defaultLocalizationsFooter);

return sb.toString();
}

const defaultLocalizationsHeader = '''

/*
* THIS FILE IS GENERATED.
* DO NOT MODIFY IT BY HAND UNLESS YOU KNOW WHAT YOU ARE DOING.
*
* See README.md for instructions on how to generate this file.
*/

import 'package:flutter/material.dart';

import 'lang/en.dart';

/// An abstract class containing all labels that concrete languages should
/// provide.
///
/// The easiest way to override some of these labels is to provide
/// an object that extends [DefaultLocalizations] and pass it to the
/// [MaterialApp.localizationsDelegates].
///
/// ```dart
/// import 'package:firebase_ui_localizations/firebase_ui_localizations.dart';
///
/// class LabelOverrides extends DefaultLocalizations {
/// const LabelOverrides();
///
/// @override
/// String get emailInputLabel => 'Enter your email';
/// }
///
/// MaterialApp(
/// // ...
/// localizationsDelegates: [
/// FirebaseUILocalizations.withDefaultOverrides(const LabelOverrides()),
/// GlobalMaterialLocalizations.delegate,
/// GlobalWidgetsLocalizations.delegate,
/// FirebaseUILocalizations.delegate,
/// ],
/// )
/// ```''';

const defaultLocalizationsFooter = '''
class DefaultLocalizations extends EnLocalizations {
const DefaultLocalizations();
}
''';

List<String> breakIntoLines(String string, int lineLength) {
final lines = <String>[];
final words = string.split(' ');

var currentLine = StringBuffer();
for (var word in words) {
if (currentLine.length + word.length > lineLength) {
lines.add(currentLine.toString());
currentLine = StringBuffer();
}

currentLine.write('$word ');
}

if (currentLine.isNotEmpty) {
lines.add(currentLine.toString());
}

return lines;
}
22 changes: 21 additions & 1 deletion packages/firebase_ui_localizations/lib/l10n/firebase_ui_ar.arb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"@@locale": "ar",
"@@last_modified": "2023-09-06T14:33:29.121119",
"@@last_modified": "2023-09-22T14:51:00.799791",
"accessDisabledErrorText": "تم إيقاف إذن الوصول إلى هذا الحساب مؤقتًا.",
"@accessDisabledErrorText": {
"description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).",
Expand Down Expand Up @@ -488,5 +488,25 @@
"@invalidVerificationCodeErrorText": {
"description": "Error text indicating that entered SMS code is invalid",
"placeholders": {}
},
"ulinkProviderAlertTitle": "Unlink provider",
"@ulinkProviderAlertTitle": {
"description": "Title that is shown in AlertDialog asking for provider unlink confirmation",
"placeholders": {}
},
"confirmUnlinkButtonLabel": "Unlink",
"@confirmUnlinkButtonLabel": {
"description": "Unlink confirmation button label",
"placeholders": {}
},
"cancelButtonLabel": "Cancel",
"@cancelButtonLabel": {
"description": "Cancel button label",
"placeholders": {}
},
"unlinkProviderAlertMessage": "Are you sure you want to unlink this provider?",
"@unlinkProviderAlertMessage": {
"description": "Text that is shown as a message of the AlertDialog confirming provider unlink",
"placeholders": {}
}
}
22 changes: 21 additions & 1 deletion packages/firebase_ui_localizations/lib/l10n/firebase_ui_de.arb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"@@locale": "de",
"@@last_modified": "2023-09-06T14:33:29.117252",
"@@last_modified": "2023-09-22T14:51:00.791691",
"accessDisabledErrorText": "Der Zugriff auf dieses Konto wurde vorübergehend gesperrt",
"@accessDisabledErrorText": {
"description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).",
Expand Down Expand Up @@ -488,5 +488,25 @@
"@invalidVerificationCodeErrorText": {
"description": "Error text indicating that entered SMS code is invalid",
"placeholders": {}
},
"ulinkProviderAlertTitle": "Unlink provider",
"@ulinkProviderAlertTitle": {
"description": "Title that is shown in AlertDialog asking for provider unlink confirmation",
"placeholders": {}
},
"confirmUnlinkButtonLabel": "Unlink",
"@confirmUnlinkButtonLabel": {
"description": "Unlink confirmation button label",
"placeholders": {}
},
"cancelButtonLabel": "Cancel",
"@cancelButtonLabel": {
"description": "Cancel button label",
"placeholders": {}
},
"unlinkProviderAlertMessage": "Are you sure you want to unlink this provider?",
"@unlinkProviderAlertMessage": {
"description": "Text that is shown as a message of the AlertDialog confirming provider unlink",
"placeholders": {}
}
}
Loading