From 267fe9664486c293dc61eade1b7cb0a4912f438b Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 19 Nov 2023 10:35:44 +0200 Subject: [PATCH] Add CTA to Chrome task extension --- lib/constants.dart | 5 +++ lib/redux/app/app_actions.dart | 2 ++ lib/redux/ui/pref_reducer.dart | 8 +++++ lib/redux/ui/pref_state.dart | 6 +++- lib/redux/ui/pref_state.g.dart | 22 ++++++++++++ lib/ui/task/task_screen.dart | 66 ++++++++++++++++++++++++++++++---- lib/utils/i18n.dart | 15 ++++++++ lib/utils/platforms.dart | 7 ++++ 8 files changed, 123 insertions(+), 8 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index 63598100c..5dca65451 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -53,6 +53,11 @@ const String kFacebookUrl = 'https://www.facebook.com/invoiceninja'; const String kYouTubeUrl = 'https://www.youtube.com/channel/UCXAHcBvhW05PDtWYIq7WDFA/videos'; +const String kTaskExtensionUrl = + 'https://chromewebstore.google.com/detail/invoice-ninja-tasks/dlfcbfdpemfnjbjlladogijcchfmmaaf'; +const String kTaskExtensionYouTubeUrl = + 'https://www.youtube.com/watch?v=UL0OklMJTEA&ab_channel=InvoiceNinja'; + const String kAppleOAuthClientId = 'com.invoiceninja.client'; const String kAppleOAuthRedirectUrl = 'https://invoicing.co/auth/apple'; diff --git a/lib/redux/app/app_actions.dart b/lib/redux/app/app_actions.dart index b0ba340ab..003cc0cb7 100644 --- a/lib/redux/app/app_actions.dart +++ b/lib/redux/app/app_actions.dart @@ -116,6 +116,8 @@ class DismissOneYearReviewAppPermanently implements PersistUI, PersistPrefs {} class DismissTwoYearReviewAppPermanently implements PersistUI, PersistPrefs {} +class DismissTaskExtensionBanner implements PersistUI, PersistPrefs {} + class ViewMainScreen { ViewMainScreen({this.addDelay = false}); diff --git a/lib/redux/ui/pref_reducer.dart b/lib/redux/ui/pref_reducer.dart index b2c43077c..f3f0ef4b3 100644 --- a/lib/redux/ui/pref_reducer.dart +++ b/lib/redux/ui/pref_reducer.dart @@ -68,6 +68,8 @@ PrefState prefReducer( historySidebarReducer(state.historySidebarMode, action) ..hideDesktopWarning = hideDesktopWarningReducer(state.hideDesktopWarning, action) + ..hideTaskExtensionBanner = + hideTaskExtensionBannerReducer(state.hideTaskExtensionBanner, action) ..hideGatewayWarning = hideGatewayWarningReducer(state.hideGatewayWarning, action) ..hideReviewApp = hideReviewAppReducer(state.hideReviewApp, action) @@ -260,6 +262,12 @@ Reducer hideDesktopWarningReducer = combineReducers([ }), ]); +Reducer hideTaskExtensionBannerReducer = combineReducers([ + TypedReducer((filter, action) { + return true; + }), +]); + Reducer hideGatewayWarningReducer = combineReducers([ TypedReducer((filter, action) { return true; diff --git a/lib/redux/ui/pref_state.dart b/lib/redux/ui/pref_state.dart index 6bcf58845..c023b0ec0 100644 --- a/lib/redux/ui/pref_state.dart +++ b/lib/redux/ui/pref_state.dart @@ -43,6 +43,7 @@ abstract class PrefState implements Built { hideReviewApp: false, hideOneYearReviewApp: false, hideTwoYearReviewApp: false, + hideTaskExtensionBanner: false, showKanban: false, showPdfPreview: true, showPdfPreviewSideBySide: false, @@ -170,6 +171,8 @@ abstract class PrefState implements Built { bool get hideTwoYearReviewApp; + bool get hideTaskExtensionBanner; + bool get editAfterSaving; bool get enableNativeBrowser; @@ -288,7 +291,8 @@ abstract class PrefState implements Built { ..colorTheme = kColorThemeLight ..darkColorTheme = kColorThemeDark ..enableDarkModeSystem = false - ..donwloadsFolder = ''; + ..donwloadsFolder = '' + ..hideTaskExtensionBanner = false; static Serializer get serializer => _$prefStateSerializer; } diff --git a/lib/redux/ui/pref_state.g.dart b/lib/redux/ui/pref_state.g.dart index 3af76cb84..cdaf512b5 100644 --- a/lib/redux/ui/pref_state.g.dart +++ b/lib/redux/ui/pref_state.g.dart @@ -220,6 +220,9 @@ class _$PrefStateSerializer implements StructuredSerializer { 'hideTwoYearReviewApp', serializers.serialize(object.hideTwoYearReviewApp, specifiedType: const FullType(bool)), + 'hideTaskExtensionBanner', + serializers.serialize(object.hideTaskExtensionBanner, + specifiedType: const FullType(bool)), 'editAfterSaving', serializers.serialize(object.editAfterSaving, specifiedType: const FullType(bool)), @@ -402,6 +405,10 @@ class _$PrefStateSerializer implements StructuredSerializer { result.hideTwoYearReviewApp = serializers.deserialize(value, specifiedType: const FullType(bool))! as bool; break; + case 'hideTaskExtensionBanner': + result.hideTaskExtensionBanner = serializers.deserialize(value, + specifiedType: const FullType(bool))! as bool; + break; case 'editAfterSaving': result.editAfterSaving = serializers.deserialize(value, specifiedType: const FullType(bool))! as bool; @@ -742,6 +749,8 @@ class _$PrefState extends PrefState { @override final bool hideTwoYearReviewApp; @override + final bool hideTaskExtensionBanner; + @override final bool editAfterSaving; @override final bool enableNativeBrowser; @@ -792,6 +801,7 @@ class _$PrefState extends PrefState { required this.hideReviewApp, required this.hideOneYearReviewApp, required this.hideTwoYearReviewApp, + required this.hideTaskExtensionBanner, required this.editAfterSaving, required this.enableNativeBrowser, required this.textScaleFactor, @@ -865,6 +875,8 @@ class _$PrefState extends PrefState { hideOneYearReviewApp, r'PrefState', 'hideOneYearReviewApp'); BuiltValueNullFieldError.checkNotNull( hideTwoYearReviewApp, r'PrefState', 'hideTwoYearReviewApp'); + BuiltValueNullFieldError.checkNotNull( + hideTaskExtensionBanner, r'PrefState', 'hideTaskExtensionBanner'); BuiltValueNullFieldError.checkNotNull( editAfterSaving, r'PrefState', 'editAfterSaving'); BuiltValueNullFieldError.checkNotNull( @@ -924,6 +936,7 @@ class _$PrefState extends PrefState { hideReviewApp == other.hideReviewApp && hideOneYearReviewApp == other.hideOneYearReviewApp && hideTwoYearReviewApp == other.hideTwoYearReviewApp && + hideTaskExtensionBanner == other.hideTaskExtensionBanner && editAfterSaving == other.editAfterSaving && enableNativeBrowser == other.enableNativeBrowser && textScaleFactor == other.textScaleFactor && @@ -971,6 +984,7 @@ class _$PrefState extends PrefState { _$hash = $jc(_$hash, hideReviewApp.hashCode); _$hash = $jc(_$hash, hideOneYearReviewApp.hashCode); _$hash = $jc(_$hash, hideTwoYearReviewApp.hashCode); + _$hash = $jc(_$hash, hideTaskExtensionBanner.hashCode); _$hash = $jc(_$hash, editAfterSaving.hashCode); _$hash = $jc(_$hash, enableNativeBrowser.hashCode); _$hash = $jc(_$hash, textScaleFactor.hashCode); @@ -1018,6 +1032,7 @@ class _$PrefState extends PrefState { ..add('hideReviewApp', hideReviewApp) ..add('hideOneYearReviewApp', hideOneYearReviewApp) ..add('hideTwoYearReviewApp', hideTwoYearReviewApp) + ..add('hideTaskExtensionBanner', hideTaskExtensionBanner) ..add('editAfterSaving', editAfterSaving) ..add('enableNativeBrowser', enableNativeBrowser) ..add('textScaleFactor', textScaleFactor) @@ -1199,6 +1214,11 @@ class PrefStateBuilder implements Builder { set hideTwoYearReviewApp(bool? hideTwoYearReviewApp) => _$this._hideTwoYearReviewApp = hideTwoYearReviewApp; + bool? _hideTaskExtensionBanner; + bool? get hideTaskExtensionBanner => _$this._hideTaskExtensionBanner; + set hideTaskExtensionBanner(bool? hideTaskExtensionBanner) => + _$this._hideTaskExtensionBanner = hideTaskExtensionBanner; + bool? _editAfterSaving; bool? get editAfterSaving => _$this._editAfterSaving; set editAfterSaving(bool? editAfterSaving) => @@ -1272,6 +1292,7 @@ class PrefStateBuilder implements Builder { _hideReviewApp = $v.hideReviewApp; _hideOneYearReviewApp = $v.hideOneYearReviewApp; _hideTwoYearReviewApp = $v.hideTwoYearReviewApp; + _hideTaskExtensionBanner = $v.hideTaskExtensionBanner; _editAfterSaving = $v.editAfterSaving; _enableNativeBrowser = $v.enableNativeBrowser; _textScaleFactor = $v.textScaleFactor; @@ -1344,6 +1365,7 @@ class PrefStateBuilder implements Builder { hideReviewApp: BuiltValueNullFieldError.checkNotNull(hideReviewApp, r'PrefState', 'hideReviewApp'), hideOneYearReviewApp: BuiltValueNullFieldError.checkNotNull(hideOneYearReviewApp, r'PrefState', 'hideOneYearReviewApp'), hideTwoYearReviewApp: BuiltValueNullFieldError.checkNotNull(hideTwoYearReviewApp, r'PrefState', 'hideTwoYearReviewApp'), + hideTaskExtensionBanner: BuiltValueNullFieldError.checkNotNull(hideTaskExtensionBanner, r'PrefState', 'hideTaskExtensionBanner'), editAfterSaving: BuiltValueNullFieldError.checkNotNull(editAfterSaving, r'PrefState', 'editAfterSaving'), enableNativeBrowser: BuiltValueNullFieldError.checkNotNull(enableNativeBrowser, r'PrefState', 'enableNativeBrowser'), textScaleFactor: BuiltValueNullFieldError.checkNotNull(textScaleFactor, r'PrefState', 'textScaleFactor'), diff --git a/lib/ui/task/task_screen.dart b/lib/ui/task/task_screen.dart index 90fc22cc7..14b91ebcc 100644 --- a/lib/ui/task/task_screen.dart +++ b/lib/ui/task/task_screen.dart @@ -1,9 +1,11 @@ // Flutter imports: +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/redux/task_status/task_status_selectors.dart'; +import 'package:invoiceninja_flutter/ui/app/icon_text.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; // Project imports: @@ -23,6 +25,7 @@ import 'package:invoiceninja_flutter/ui/task/task_screen_vm.dart'; import 'package:invoiceninja_flutter/utils/icons.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; +import 'package:url_launcher/url_launcher.dart'; class TaskScreen extends StatelessWidget { const TaskScreen({ @@ -40,18 +43,18 @@ class TaskScreen extends StatelessWidget { final state = store.state; final company = store.state.company; final userCompany = store.state.userCompany; - final localization = AppLocalization.of(context); + final localization = AppLocalization.of(context)!; final statuses = [ TaskStatusEntity().rebuild((b) => b ..id = kTaskStatusLogged - ..name = localization!.logged), + ..name = localization.logged), TaskStatusEntity().rebuild((b) => b ..id = kTaskStatusRunning - ..name = localization!.running), + ..name = localization.running), if (!state.prefState.showKanban) TaskStatusEntity().rebuild((b) => b ..id = kTaskStatusInvoiced - ..name = localization!.invoiced), + ..name = localization.invoiced), for (var statusId in memoizedSortedActiveTaskStatusIds( state.taskStatusState.list, state.taskStatusState.map)) TaskStatusEntity().rebuild((b) => b @@ -99,8 +102,57 @@ class TaskScreen extends StatelessWidget { }, ) ], - body: - state.prefState.showKanban ? KanbanViewBuilder() : TaskListBuilder(), + body: Column(children: [ + // TODO once Firefox is supported + if (!state.prefState.hideTaskExtensionBanner && + isDesktop(context) && + (!kIsWeb || isChrome())) + ColoredBox( + color: Colors.orange, + child: Row( + children: [ + SizedBox(width: 16), + Expanded( + child: IconText( + text: localization.taskExtensionBanner, + icon: Icons.info_outline, + ), + ), + TextButton( + onPressed: () { + launchUrl(Uri.parse(kTaskExtensionYouTubeUrl)); + }, + child: Text( + localization.watchVideo, + style: TextStyle(color: Colors.white), + ), + ), + TextButton( + onPressed: () { + launchUrl(Uri.parse(kTaskExtensionUrl)); + }, + child: Text( + localization.viewExtension, + style: TextStyle(color: Colors.white), + ), + ), + IconButton( + tooltip: localization.dismiss, + onPressed: () { + store.dispatch(DismissTaskExtensionBanner()); + }, + icon: Icon(Icons.clear), + ), + SizedBox(width: 12), + ], + ), + ), + Expanded( + child: state.prefState.showKanban + ? KanbanViewBuilder() + : TaskListBuilder(), + ), + ]), bottomNavigationBar: AppBottomBar( entityType: EntityType.task, iconButtons: [ @@ -166,7 +218,7 @@ class TaskScreen extends StatelessWidget { Icons.add, color: Colors.white, ), - tooltip: localization!.newTask, + tooltip: localization.newTask, ) : null, ); diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 5c0af8ea8..b6fda2f9d 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,6 +18,9 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'task_extension_banner': 'Add the Chrome extension to manage your tasks', + 'watch_video': 'Watch Video', + 'view_extension': 'View Extension', 'reactivate_email': 'Reactivate Email', 'email_reactivated': 'Successfully reactivated email', 'template_help': 'Enable using the design as a template', @@ -109980,6 +109983,18 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]!['email_reactivated'] ?? _localizedValues['en']!['email_reactivated']!; + String get taskExtensionBanner => + _localizedValues[localeCode]!['task_extension_banner'] ?? + _localizedValues['en']!['task_extension_banner']!; + + String get watchVideo => + _localizedValues[localeCode]!['watch_video'] ?? + _localizedValues['en']!['watch_video']!; + + String get viewExtension => + _localizedValues[localeCode]!['view_extension'] ?? + _localizedValues['en']!['view_extension']!; + // STARTER: lang field - do not remove comment String lookup(String? key) { diff --git a/lib/utils/platforms.dart b/lib/utils/platforms.dart index 02e448b8c..46fa467d3 100644 --- a/lib/utils/platforms.dart +++ b/lib/utils/platforms.dart @@ -191,6 +191,13 @@ String getPlatformName() { return 'Unknown'; } +bool isChrome() { + String userAgent = WebUtils.getHtmlValue('user-agent') ?? ''; + userAgent = userAgent.toLowerCase(); + + return userAgent.contains('chrome'); +} + String getNativePlatform() { String userAgent = WebUtils.getHtmlValue('user-agent') ?? ''; userAgent = userAgent.toLowerCase();