Skip to content

Commit

Permalink
#17 explicitly explain to a user why do we request those google permi…
Browse files Browse the repository at this point in the history
…ssions
  • Loading branch information
Denis Zhdanov committed Jan 1, 2025
1 parent 4b9c62b commit 8874c91
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 30 deletions.
8 changes: 8 additions & 0 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
7A86921416E7E45FF352EBA8 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8F166BC65D95B65548923E6F /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
928D6E262D25380700E851C5 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -142,6 +143,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
928D6E262D25380700E851C5 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
Expand Down Expand Up @@ -511,6 +513,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
Expand All @@ -519,6 +522,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Chrono Sheet";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -706,6 +710,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
Expand All @@ -714,6 +719,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Chrono Sheet";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand All @@ -739,6 +745,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
Expand All @@ -747,6 +754,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Chrono Sheet";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
8 changes: 8 additions & 0 deletions ios/Runner/Runner.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
125 changes: 99 additions & 26 deletions lib/google/google_helper.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import 'dart:convert';

import 'package:chrono_sheet/generated/app_localizations.dart';
import 'package:chrono_sheet/logging/logging.dart';
import 'package:chrono_sheet/main.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis/sheets/v4.dart' as sheets;
import 'package:http/src/client.dart' as http;
import 'package:googleapis/drive/v3.dart' as drive;
import 'package:http/http.dart' as http;
import '../http/AuthenticatedHttpClient.dart';
import '../util/rich_text_util.dart';

final _logger = getNamedLogger();

final signIn = GoogleSignIn(scopes: [sheets.SheetsApi.spreadsheetsScope, sheets.SheetsApi.driveFileScope]);
final signIn = GoogleSignIn(scopes: [
sheets.SheetsApi.spreadsheetsScope,
drive.DriveApi.driveScope,
]);
http.Client? _clientOverride;

void setClientOverride(http.Client client) {
Expand All @@ -24,30 +30,97 @@ Future<http.Client> getAuthenticatedGoogleApiHttpClient() async {
}
_logger.fine("trying to sign in silently");
var googleAccount = await signIn.signInSilently();
if (googleAccount == null) {
_logger.fine("failed to sign in silently, signing in normally");
await showDialog(
context: navigatorKey.currentContext!,
builder: (context) {
final theme = Theme.of(context);
return AlertDialog(
content: buildRichText(AppLocalizations.of(context).textPermissionsRationale, theme.textTheme),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(AppLocalizations.of(context).textOk),
),
],
);
},
);
googleAccount = await signIn.signIn();
} else {
_logger.fine("successfully signed in silently");
while (true) {
if (googleAccount == null) {
_logger.info("failed to sign in silently, signing in normally");
await _showPermissionsRationale();
googleAccount = await signIn.signIn();
} else {
_logger.info("successfully signed in silently");
}
if (googleAccount == null) {
_logger.info("failed to explicitly sign in into google");
continue;
}

Set<String> missingScopes = await _getMissingScopes(googleAccount);
if (missingScopes.isNotEmpty) {
_logger.info("detected that the user didn't provide the following scopes during google login: $missingScopes");
await _showPermissionsRationale(missingScopes);
final granted = await signIn.requestScopes(missingScopes.toList());
if (!granted) {
_logger.info("didn't get required scopes during google login even after explicit request");
continue;
}
googleAccount = await signIn.signInSilently();
} else {
final headers = await googleAccount.authHeaders;
return AuthenticatedHttpClient(headers);
}
}
if (googleAccount == null) {
throw StateError("can not login into google");
}

Future<void> _showPermissionsRationale([Set<String> missingScopes = const {}]) {
return showDialog(
context: navigatorKey.currentContext!,
builder: (context) {
final l10n = AppLocalizations.of(context);
final theme = Theme.of(context);
String title;
if (missingScopes.isEmpty) {
title = l10n.titlePermissionsRationale;
} else {
title = l10n.errorPermissionsNotGranted;
for (final scope in missingScopes) {
title += "\n * **";
switch (scope) {
case drive.DriveApi.driveScope:
title += l10n.permissionDrive;
break;
case sheets.SheetsApi.spreadsheetsScope:
title += l10n.permissionSheets;
break;
default:
title += scope;
}
title += "**";
}
}
return AlertDialog(
title: Row(children: [
Icon(Icons.warning, color: Colors.red, size: theme.textTheme.headlineLarge?.fontSize ?? 32.0),
SizedBox(width: 16),
Expanded(child: buildRichText(title, theme.textTheme))
]),
content: buildRichText(AppLocalizations.of(context).textPermissionsRationale, theme.textTheme),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(AppLocalizations.of(context).textOk),
),
],
);
},
);
}

Future<Set<String>> _getMissingScopes(GoogleSignInAccount account) async {
final auth = await account.authentication;
final token = auth.accessToken;
final response = await http.get(
Uri.parse('https://oauth2.googleapis.com/tokeninfo?access_token=$token'),
);
final result = Set.of(signIn.scopes);
if (response.statusCode == 200) {
final tokenInfo = json.decode(response.body);
final scopes = tokenInfo["scope"]?.toString();
if (scopes != null) {
for (final scope in signIn.scopes) {
if (scopes.contains(scope)) {
result.remove(scope);
}
}
}
}
final headers = await googleAccount.authHeaders;
return AuthenticatedHttpClient(headers);
return result;
}
2 changes: 1 addition & 1 deletion lib/google/state/google_login_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class LoginState extends _$LoginState {
Future<void> logout() async {
_logger.fine("got a request to logout");
state = AsyncValue.loading();
await signIn.signOut();
await signIn.disconnect();
state = AsyncValue.data(false);
}
}
7 changes: 6 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
"titleSelectFile": "Select Google Sheet File",
"titleAddNewCategory": "Add New Category",
"titleAddNewFile": "Add New File",
"titlePermissionsRationale": "**Your Permissions Enable Key Features**",

"textReset": "Reset",
"textSave": "Save",
"textCancel": "Cancel",
"textAdd": "Add",
"textOk": "Ok",
"textClose": "Close",
"textPermissionsRationale": "**Why Does This App Need Permissions?**\n\nOur app requires access to your Google Drive and Google Sheets to function properly:\n\n 1. **Google Drive:** To show you a list of your existing Google Sheets and let you create a new one if needed.\n\n 2. **Google Sheets:** To store your stopwatch measurements directly in your chosen Google Sheet.\n\nWe only access the files you select or create through the app. Your data remains private and secure.",
"textPermissionsRationale": "Our app requires access to your Google Drive and Google Sheets to function properly:\n\n 1. **Google Drive:** To show you a list of your existing Google Sheets and let you create a new one if needed.\n\n 2. **Google Sheets:** To store your stopwatch measurements directly in your chosen Google Sheet.\n\nWe only access the files you select or create through the app. Your data remains private and secure.",

"hintCreateCategory": "create a category",
"hintSelectFile": "tap to select a file",
Expand All @@ -22,6 +23,10 @@
"errorCanNotParseCategories": "can not parse categories",
"errorNoFileIsSelected": "no file is selected",
"errorNoCategoryIsSelected": "no category is selected",
"errorPermissionsNotGranted": "The following mandatory permissions are not granted:\n",

"permissionDrive": "Google Drive",
"permissionSheets": "Google Sheets",

"progressFileCreationInProgress": "file creation is in progress...",
"progressParsingCategories": "parsing categories from file..."
Expand Down
2 changes: 1 addition & 1 deletion lib/util/rich_text_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ RichText buildRichText(String text, TextTheme theme) {
while (offset < text.length) {
int markerStart = text.indexOf(_Marker.bold, offset);
if (markerStart >= 0) {
int markerEnd = text.indexOf(_Marker.bold, markerStart + 1);
int markerEnd = text.indexOf(_Marker.bold, markerStart + _Marker.bold.length);
if (markerEnd > markerStart) {
if (offset < markerStart) {
spans.add(TextSpan(
Expand Down
2 changes: 1 addition & 1 deletion pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ packages:
source: hosted
version: "4.2.0"
http:
dependency: transitive
dependency: "direct main"
description:
name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies:
firebase_analytics: 11.3.6
shared_preferences: 2.3.4
uuid: 4.5.1
http: 1.2.2

dev_dependencies:
flutter_test:
Expand Down

0 comments on commit 8874c91

Please sign in to comment.