Use the owned login WebView for first login too, not just add-account
Build APK / build (push) Successful in 5m5s
Build APK / build (push) Successful in 5m5s
The Chrome Custom Tab path for the first/only login had real costs - no way to close it automatically on success, and it's a separate task outside the app's own navigation - that only bought Chrome's own autofill in exchange. LoginWebViewView now handles every login, not just add-account, which was already using it to avoid a Custom Tab silently reusing Chrome's session for a different account. url_launcher is no longer a dependency as a result. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+17
-28
@@ -9,34 +9,23 @@ via [`LoginFlowService`](../../lib/services/login_flow_service.dart):
|
|||||||
1. `LoginFlowService.initiate(serverUrl)` POSTs to
|
1. `LoginFlowService.initiate(serverUrl)` POSTs to
|
||||||
`{server}/index.php/login/v2`, gets back a browser login URL + a poll
|
`{server}/index.php/login/v2`, gets back a browser login URL + a poll
|
||||||
endpoint/token.
|
endpoint/token.
|
||||||
2. The app opens the login URL for the user to authenticate/authorize.
|
2. The app opens the login URL for the user to authenticate/authorize in
|
||||||
Which browser depends on whether this is the first login or an
|
[`LoginWebViewView`](../../lib/views/login_webview_view.dart), a normal
|
||||||
add-account flow (`addAccount`, see below):
|
screen this app owns (`package:webview_flutter`) - pushed by `LoginView`
|
||||||
- First/only login: a Chrome Custom Tab (`url_launcher`,
|
the moment `loginFlowStatus` flips to `awaitingBrowser`, for every login
|
||||||
`LaunchMode.inAppBrowserView`) — real Chrome, so saved
|
(first account or an additional one), not just add-account. This used
|
||||||
passwords/autofill work, unlike Flutter's own embedded web view.
|
to be split: first login went through a Chrome Custom Tab
|
||||||
There's no way to close the tab automatically on success (Login Flow
|
(`url_launcher`, `LaunchMode.inAppBrowserView`) for Chrome's own
|
||||||
v2 never redirects back into the app, and a Custom Tab belongs to
|
autofill, and only add-account used the embedded WebView, specifically
|
||||||
Chrome's own task) — the user switches back manually.
|
to avoid a Custom Tab silently reusing Chrome's existing session for a
|
||||||
- Adding another account: [`LoginWebViewView`](../../lib/views/login_webview_view.dart),
|
different account. But a Custom Tab has real costs even for the first
|
||||||
a normal screen this app owns, backed by `package:webview_flutter`
|
login - no way to close it automatically on success (the user has to
|
||||||
rather than a Custom Tab - pushed by `LoginView` the moment
|
switch back manually), and it's a separate task outside this app's own
|
||||||
`loginFlowStatus` flips to `awaitingBrowser`. Two reasons this isn't
|
navigation entirely - so both paths now use the same owned screen.
|
||||||
a Custom Tab: (1) a Custom Tab shares Chrome's actual browser
|
`url_launcher` is no longer a dependency. The cost is no
|
||||||
profile/cookie jar - if the user is still logged into the first
|
Chrome-autofill (Android's own system Autofill framework, e.g. a
|
||||||
account on the Nextcloud web UI in Chrome, it would silently reuse
|
password manager, may still work in the WebView; Chrome's own
|
||||||
that session and authorize the wrong account instead of prompting
|
saved-password autofill specifically cannot, since that's Chrome-only).
|
||||||
fresh credentials; (2) `url_launcher`'s own `LaunchMode.inAppWebView`
|
|
||||||
(tried first) is a bare native WebView Activity with no chrome of its
|
|
||||||
own - no close button, and on at least some devices it draws
|
|
||||||
edge-to-edge and hides the status bar. Owning the screen fixes both -
|
|
||||||
isolated cookies, a normal `AppBar`/close button/safe area - and lets
|
|
||||||
it close itself automatically on success (it watches
|
|
||||||
`loginFlowStatus` itself), unlike the Custom Tab case. The cost is no
|
|
||||||
Chrome-autofill for this one flow (Android's own system Autofill
|
|
||||||
framework, e.g. a password manager, may still work in the WebView;
|
|
||||||
Chrome's own saved-password autofill specifically cannot, since
|
|
||||||
that's Chrome-only).
|
|
||||||
3. `ServerProvider` polls `LoginFlowService.poll(pollEndpoint, token)` every
|
3. `ServerProvider` polls `LoginFlowService.poll(pollEndpoint, token)` every
|
||||||
2 seconds (`Timer.periodic`, see `_pollTimer`/`_pollTimeoutTimer` in
|
2 seconds (`Timer.periodic`, see `_pollTimer`/`_pollTimeoutTimer` in
|
||||||
`server_provider.dart`) until it gets a 200 with `server`/`loginName`/
|
`server_provider.dart`) until it gets a 200 with `server`/`loginName`/
|
||||||
|
|||||||
@@ -70,12 +70,6 @@
|
|||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
<!-- Needed so url_launcher can find a browser for Nextcloud Login Flow v2. -->
|
|
||||||
<intent>
|
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
|
||||||
<data android:scheme="https"/>
|
|
||||||
</intent>
|
|
||||||
<!-- Needed so open_file can find apps to open downloaded files. -->
|
<!-- Needed so open_file can find apps to open downloaded files. -->
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:convert';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
import '../models/app_tab.dart';
|
import '../models/app_tab.dart';
|
||||||
import '../models/nextcloud_file_version.dart';
|
import '../models/nextcloud_file_version.dart';
|
||||||
import '../models/nextcloud_item.dart';
|
import '../models/nextcloud_item.dart';
|
||||||
@@ -705,13 +704,26 @@ class ServerProvider extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Starts Nextcloud Login Flow v2: asks the server for a one-time login
|
/// Starts Nextcloud Login Flow v2: asks the server for a one-time login
|
||||||
/// URL, opens it in the system browser, then polls until the user
|
/// URL, then polls until the user authorizes (in LoginWebViewView, pushed
|
||||||
/// authorizes and the server hands back a scoped app password. The app
|
/// by LoginView once loginFlowStatus flips to awaitingBrowser below) and
|
||||||
/// never sees the user's real password. [addAccount] only marks the
|
/// the server hands back a scoped app password. The app never sees the
|
||||||
/// flow as "add another account" for [isAddAccountFlow] - the pushed Add
|
/// user's real password. [addAccount] only marks the flow as "add
|
||||||
/// Account screen reads that to decide when to pop itself; this method's
|
/// another account" for [isAddAccountFlow] - the pushed Add Account
|
||||||
/// own persistence behavior on success ([_completeLoginFlow]) is the same
|
/// screen reads that to decide when to pop itself; this method's own
|
||||||
/// either way (create-or-refresh the resulting account, then activate it).
|
/// persistence behavior on success ([_completeLoginFlow]) is the same
|
||||||
|
/// either way (create-or-refresh the resulting account, then activate
|
||||||
|
/// it).
|
||||||
|
///
|
||||||
|
/// Every login - first account or an additional one - goes through
|
||||||
|
/// LoginWebViewView (`package:webview_flutter`), a screen this app owns,
|
||||||
|
/// rather than a Chrome Custom Tab. That used to only be true for
|
||||||
|
/// add-account, specifically to avoid a Custom Tab silently reusing
|
||||||
|
/// Chrome's existing session for a different account; but a Custom Tab
|
||||||
|
/// has real costs even for the first login - no way to close it
|
||||||
|
/// automatically on success (the user has to switch back manually), and
|
||||||
|
/// it's a separate task outside this app's own navigation entirely. The
|
||||||
|
/// only thing it bought over a plain WebView was Chrome's own
|
||||||
|
/// autofill/saved-password support, which isn't worth those tradeoffs.
|
||||||
Future<void> startLoginFlow(
|
Future<void> startLoginFlow(
|
||||||
String serverUrl, {
|
String serverUrl, {
|
||||||
bool addAccount = false,
|
bool addAccount = false,
|
||||||
@@ -724,42 +736,6 @@ class ServerProvider extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
try {
|
try {
|
||||||
final init = await LoginFlowService.initiate(serverUrl);
|
final init = await LoginFlowService.initiate(serverUrl);
|
||||||
_pendingLoginUrl = init.loginUrl;
|
_pendingLoginUrl = init.loginUrl;
|
||||||
|
|
||||||
// Chrome Custom Tabs (inAppBrowserView) for the first/only login:
|
|
||||||
// real Chrome, so the saved passwords/autofill service works
|
|
||||||
// normally, unlike Flutter's own embedded web view. The tradeoff is
|
|
||||||
// that - like Login Flow v2's grant page itself, which never
|
|
||||||
// redirects back into the app on its own (unlike the old nc://
|
|
||||||
// Flow v1) - a Custom Tab belongs to Chrome's own task, not ours, so
|
|
||||||
// we have no way to close it automatically once polling below
|
|
||||||
// detects success; the user has to switch back manually, same as
|
|
||||||
// with the external browser.
|
|
||||||
//
|
|
||||||
// Adding another account instead shows the login page in
|
|
||||||
// LoginWebViewView, a real screen this app owns (pushed by LoginView
|
|
||||||
// once loginFlowStatus flips to awaitingBrowser below), backed by
|
|
||||||
// Flutter's own WebView rather than a Custom Tab. Two reasons, not
|
|
||||||
// just one: (1) a Custom Tab shares Chrome's actual browser
|
|
||||||
// profile/cookie jar, so if the user is still logged into the first
|
|
||||||
// account on the Nextcloud web UI in Chrome, it would silently reuse
|
|
||||||
// that session and authorize the wrong account instead of prompting
|
|
||||||
// fresh credentials; (2) url_launcher's own LaunchMode.inAppWebView
|
|
||||||
// is a bare native WebView Activity with no chrome of its own - no
|
|
||||||
// close button, and on at least some devices it draws edge-to-edge
|
|
||||||
// and hides the status bar. Owning the screen ourselves fixes both:
|
|
||||||
// isolated cookies, plus a normal AppBar/close button/safe area, and
|
|
||||||
// as a bonus we CAN close it automatically on success (LoginWebView
|
|
||||||
// View watches loginFlowStatus itself), unlike the Custom Tab case.
|
|
||||||
if (!addAccount) {
|
|
||||||
final opened = await launchUrl(
|
|
||||||
init.loginUrl,
|
|
||||||
mode: LaunchMode.inAppBrowserView,
|
|
||||||
);
|
|
||||||
if (!opened) {
|
|
||||||
throw Exception('Could not open the browser for login.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_loginFlowStatus = LoginFlowStatus.awaitingBrowser;
|
_loginFlowStatus = LoginFlowStatus.awaitingBrowser;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
@@ -809,15 +785,6 @@ class ServerProvider extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only relevant to the first/only-login Chrome Custom Tab path - the
|
|
||||||
// add-account LoginWebViewView is a normal pushed screen the user can't
|
|
||||||
// lose track of, so it has no equivalent "reopen" need.
|
|
||||||
Future<void> reopenLoginBrowser() async {
|
|
||||||
if (_pendingLoginUrl != null) {
|
|
||||||
await launchUrl(_pendingLoginUrl!, mode: LaunchMode.inAppBrowserView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void cancelLoginFlow({String? errorMessage}) {
|
void cancelLoginFlow({String? errorMessage}) {
|
||||||
_pollTimer?.cancel();
|
_pollTimer?.cancel();
|
||||||
_pollTimeoutTimer?.cancel();
|
_pollTimeoutTimer?.cancel();
|
||||||
|
|||||||
+12
-71
@@ -69,14 +69,12 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// The add-account flow shows its login page in LoginWebViewView (a
|
// Every login (first account or an additional one) shows its login
|
||||||
// real pushed screen, not a Custom Tab - see ServerProvider.
|
// page in LoginWebViewView, a real screen this app owns - see
|
||||||
// startLoginFlow's doc comment for why) rather than this view's own
|
// ServerProvider.startLoginFlow's doc comment for why. Push it the
|
||||||
// _WaitingForBrowser, so push it the moment there's a URL to show.
|
// moment there's a URL to show; _webViewPushed resets once that route
|
||||||
// _webViewPushed resets once that route pops (cancelled or done) so a
|
// pops (cancelled or done) so a retry after cancelling pushes it again.
|
||||||
// retry after cancelling pushes it again.
|
if (!_webViewPushed &&
|
||||||
if (widget.isAddingAccount &&
|
|
||||||
!_webViewPushed &&
|
|
||||||
isAwaitingBrowser &&
|
isAwaitingBrowser &&
|
||||||
provider.pendingLoginUrl != null) {
|
provider.pendingLoginUrl != null) {
|
||||||
_webViewPushed = true;
|
_webViewPushed = true;
|
||||||
@@ -101,12 +99,7 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 24),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 440),
|
constraints: const BoxConstraints(maxWidth: 440),
|
||||||
child: isAwaitingBrowser && !widget.isAddingAccount
|
child: Column(
|
||||||
? _WaitingForBrowser(
|
|
||||||
onCancel: () => provider.cancelLoginFlow(),
|
|
||||||
onReopenBrowser: provider.reopenLoginBrowser,
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// A logout keeps the account saved rather than
|
// A logout keeps the account saved rather than
|
||||||
@@ -117,17 +110,14 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
provider.accounts.isNotEmpty) ...[
|
provider.accounts.isNotEmpty) ...[
|
||||||
_SavedAccountsSection(
|
_SavedAccountsSection(
|
||||||
accounts: provider.accounts,
|
accounts: provider.accounts,
|
||||||
onSelect: (account) =>
|
onSelect: (account) => provider.switchAccount(account.id),
|
||||||
provider.switchAccount(account.id),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Expanded(child: Divider()),
|
const Expanded(child: Divider()),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
horizontal: 12,
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
'or',
|
'or',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
@@ -144,9 +134,7 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
formKey: _formKey,
|
formKey: _formKey,
|
||||||
urlController: _urlController,
|
urlController: _urlController,
|
||||||
isLoading:
|
isLoading:
|
||||||
isInitiating ||
|
isInitiating || provider.isLoading || isAwaitingBrowser,
|
||||||
provider.isLoading ||
|
|
||||||
(widget.isAddingAccount && isAwaitingBrowser),
|
|
||||||
errorMessage:
|
errorMessage:
|
||||||
provider.loginFlowStatus == LoginFlowStatus.error
|
provider.loginFlowStatus == LoginFlowStatus.error
|
||||||
? provider.errorMessage
|
? provider.errorMessage
|
||||||
@@ -166,7 +154,7 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
if (!widget.isAddingAccount) return body;
|
if (!widget.isAddingAccount) return body;
|
||||||
|
|
||||||
// Backing out mid-flow (system back/swipe-back, not just the explicit
|
// Backing out mid-flow (system back/swipe-back, not just the explicit
|
||||||
// Cancel button in _WaitingForBrowser) should cancel the pending login
|
// Cancel button in LoginWebViewView) should cancel the pending login
|
||||||
// flow rather than leaving its poll timer running after this screen is
|
// flow rather than leaving its poll timer running after this screen is
|
||||||
// gone - this view could never be popped before "add account" existed,
|
// gone - this view could never be popped before "add account" existed,
|
||||||
// so that case wasn't reachable until now.
|
// so that case wasn't reachable until now.
|
||||||
@@ -323,7 +311,7 @@ class _ServerForm extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
"You'll finish signing in through your browser. This app never sees your password.",
|
"You'll finish signing in on the page that opens. This app never sees your password.",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
@@ -414,50 +402,3 @@ class _SavedAccountRow extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _WaitingForBrowser extends StatelessWidget {
|
|
||||||
final VoidCallback onCancel;
|
|
||||||
final VoidCallback onReopenBrowser;
|
|
||||||
|
|
||||||
const _WaitingForBrowser({
|
|
||||||
required this.onCancel,
|
|
||||||
required this.onReopenBrowser,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final colorScheme = theme.colorScheme;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(color: colorScheme.primary),
|
|
||||||
const SizedBox(height: 28),
|
|
||||||
Text(
|
|
||||||
'Waiting for you to sign in',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: theme.textTheme.titleLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Complete the login in the browser window that just opened, then come back here.',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: onReopenBrowser,
|
|
||||||
icon: const Icon(Icons.open_in_browser_rounded),
|
|
||||||
label: const Text('Reopen browser'),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
TextButton(onPressed: onCancel, child: const Text('Cancel')),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
import '../providers/server_provider.dart';
|
import '../providers/server_provider.dart';
|
||||||
|
|
||||||
/// The "add another account" Login Flow v2 page, rendered in Flutter's own
|
/// Every Login Flow v2 page - first login or an additional account -
|
||||||
/// WebView rather than url_launcher's `LaunchMode.inAppWebView` - that mode
|
/// rendered in Flutter's own WebView rather than a Chrome Custom Tab or
|
||||||
/// hosts a bare native WebView Activity with no chrome of its own: no close
|
/// url_launcher's `LaunchMode.inAppWebView`. The latter hosts a bare native
|
||||||
/// button, and on at least some devices it draws edge-to-edge and hides the
|
/// WebView Activity with no chrome of its own: no close button, and on at
|
||||||
/// status bar with no way back short of the OS back gesture. A normal
|
/// least some devices it draws edge-to-edge and hides the status bar with
|
||||||
/// Scaffold/AppBar here gets the status bar/safe-area handling and a close
|
/// no way back short of the OS back gesture. A normal Scaffold/AppBar here
|
||||||
/// button for free, exactly like every other screen in the app.
|
/// gets the status bar/safe-area handling and a close button for free,
|
||||||
|
/// exactly like every other screen in the app - and unlike a Custom Tab,
|
||||||
|
/// this screen can close itself automatically once login succeeds.
|
||||||
///
|
///
|
||||||
/// See ServerProvider.startLoginFlow's doc comment for why the add-account
|
/// See ServerProvider.startLoginFlow's doc comment for the full rationale.
|
||||||
/// flow uses an embedded WebView (isolated cookies) instead of a Chrome
|
|
||||||
/// Custom Tab at all.
|
|
||||||
class LoginWebViewView extends StatefulWidget {
|
class LoginWebViewView extends StatefulWidget {
|
||||||
final Uri url;
|
final Uri url;
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ class _LoginWebViewViewState extends State<LoginWebViewView> {
|
|||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Add Account'),
|
title: const Text('Sign in'),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
tooltip: 'Cancel',
|
tooltip: 'Cancel',
|
||||||
|
|||||||
@@ -925,30 +925,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
url_launcher:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: url_launcher
|
|
||||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.3.2"
|
|
||||||
url_launcher_android:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: url_launcher_android
|
|
||||||
sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.3.33"
|
|
||||||
url_launcher_ios:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: url_launcher_ios
|
|
||||||
sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.4.2"
|
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -957,14 +933,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.3"
|
version: "3.2.3"
|
||||||
url_launcher_macos:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: url_launcher_macos
|
|
||||||
sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.2.6"
|
|
||||||
url_launcher_platform_interface:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ dependencies:
|
|||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
flutter_secure_storage: ^11.1.1
|
flutter_secure_storage: ^11.1.1
|
||||||
url_launcher: ^6.3.1
|
|
||||||
dio: ^5.11.1
|
dio: ^5.11.1
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
open_file: ^3.5.10
|
open_file: ^3.5.10
|
||||||
|
|||||||
Reference in New Issue
Block a user