Add CTA to Chrome task extension

This commit is contained in:
Hillel Coren 2023-11-19 10:35:44 +02:00
parent 2fb7945f23
commit 267fe96644
8 changed files with 123 additions and 8 deletions

View File

@ -53,6 +53,11 @@ const String kFacebookUrl = 'https://www.facebook.com/invoiceninja';
const String kYouTubeUrl = const String kYouTubeUrl =
'https://www.youtube.com/channel/UCXAHcBvhW05PDtWYIq7WDFA/videos'; '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 kAppleOAuthClientId = 'com.invoiceninja.client';
const String kAppleOAuthRedirectUrl = 'https://invoicing.co/auth/apple'; const String kAppleOAuthRedirectUrl = 'https://invoicing.co/auth/apple';

View File

@ -116,6 +116,8 @@ class DismissOneYearReviewAppPermanently implements PersistUI, PersistPrefs {}
class DismissTwoYearReviewAppPermanently implements PersistUI, PersistPrefs {} class DismissTwoYearReviewAppPermanently implements PersistUI, PersistPrefs {}
class DismissTaskExtensionBanner implements PersistUI, PersistPrefs {}
class ViewMainScreen { class ViewMainScreen {
ViewMainScreen({this.addDelay = false}); ViewMainScreen({this.addDelay = false});

View File

@ -68,6 +68,8 @@ PrefState prefReducer(
historySidebarReducer(state.historySidebarMode, action) historySidebarReducer(state.historySidebarMode, action)
..hideDesktopWarning = ..hideDesktopWarning =
hideDesktopWarningReducer(state.hideDesktopWarning, action) hideDesktopWarningReducer(state.hideDesktopWarning, action)
..hideTaskExtensionBanner =
hideTaskExtensionBannerReducer(state.hideTaskExtensionBanner, action)
..hideGatewayWarning = ..hideGatewayWarning =
hideGatewayWarningReducer(state.hideGatewayWarning, action) hideGatewayWarningReducer(state.hideGatewayWarning, action)
..hideReviewApp = hideReviewAppReducer(state.hideReviewApp, action) ..hideReviewApp = hideReviewAppReducer(state.hideReviewApp, action)
@ -260,6 +262,12 @@ Reducer<bool> hideDesktopWarningReducer = combineReducers([
}), }),
]); ]);
Reducer<bool> hideTaskExtensionBannerReducer = combineReducers([
TypedReducer<bool, DismissTaskExtensionBanner>((filter, action) {
return true;
}),
]);
Reducer<bool> hideGatewayWarningReducer = combineReducers([ Reducer<bool> hideGatewayWarningReducer = combineReducers([
TypedReducer<bool, DismissGatewayWarningPermanently>((filter, action) { TypedReducer<bool, DismissGatewayWarningPermanently>((filter, action) {
return true; return true;

View File

@ -43,6 +43,7 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
hideReviewApp: false, hideReviewApp: false,
hideOneYearReviewApp: false, hideOneYearReviewApp: false,
hideTwoYearReviewApp: false, hideTwoYearReviewApp: false,
hideTaskExtensionBanner: false,
showKanban: false, showKanban: false,
showPdfPreview: true, showPdfPreview: true,
showPdfPreviewSideBySide: false, showPdfPreviewSideBySide: false,
@ -170,6 +171,8 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
bool get hideTwoYearReviewApp; bool get hideTwoYearReviewApp;
bool get hideTaskExtensionBanner;
bool get editAfterSaving; bool get editAfterSaving;
bool get enableNativeBrowser; bool get enableNativeBrowser;
@ -288,7 +291,8 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
..colorTheme = kColorThemeLight ..colorTheme = kColorThemeLight
..darkColorTheme = kColorThemeDark ..darkColorTheme = kColorThemeDark
..enableDarkModeSystem = false ..enableDarkModeSystem = false
..donwloadsFolder = ''; ..donwloadsFolder = ''
..hideTaskExtensionBanner = false;
static Serializer<PrefState> get serializer => _$prefStateSerializer; static Serializer<PrefState> get serializer => _$prefStateSerializer;
} }

View File

@ -220,6 +220,9 @@ class _$PrefStateSerializer implements StructuredSerializer<PrefState> {
'hideTwoYearReviewApp', 'hideTwoYearReviewApp',
serializers.serialize(object.hideTwoYearReviewApp, serializers.serialize(object.hideTwoYearReviewApp,
specifiedType: const FullType(bool)), specifiedType: const FullType(bool)),
'hideTaskExtensionBanner',
serializers.serialize(object.hideTaskExtensionBanner,
specifiedType: const FullType(bool)),
'editAfterSaving', 'editAfterSaving',
serializers.serialize(object.editAfterSaving, serializers.serialize(object.editAfterSaving,
specifiedType: const FullType(bool)), specifiedType: const FullType(bool)),
@ -402,6 +405,10 @@ class _$PrefStateSerializer implements StructuredSerializer<PrefState> {
result.hideTwoYearReviewApp = serializers.deserialize(value, result.hideTwoYearReviewApp = serializers.deserialize(value,
specifiedType: const FullType(bool))! as bool; specifiedType: const FullType(bool))! as bool;
break; break;
case 'hideTaskExtensionBanner':
result.hideTaskExtensionBanner = serializers.deserialize(value,
specifiedType: const FullType(bool))! as bool;
break;
case 'editAfterSaving': case 'editAfterSaving':
result.editAfterSaving = serializers.deserialize(value, result.editAfterSaving = serializers.deserialize(value,
specifiedType: const FullType(bool))! as bool; specifiedType: const FullType(bool))! as bool;
@ -742,6 +749,8 @@ class _$PrefState extends PrefState {
@override @override
final bool hideTwoYearReviewApp; final bool hideTwoYearReviewApp;
@override @override
final bool hideTaskExtensionBanner;
@override
final bool editAfterSaving; final bool editAfterSaving;
@override @override
final bool enableNativeBrowser; final bool enableNativeBrowser;
@ -792,6 +801,7 @@ class _$PrefState extends PrefState {
required this.hideReviewApp, required this.hideReviewApp,
required this.hideOneYearReviewApp, required this.hideOneYearReviewApp,
required this.hideTwoYearReviewApp, required this.hideTwoYearReviewApp,
required this.hideTaskExtensionBanner,
required this.editAfterSaving, required this.editAfterSaving,
required this.enableNativeBrowser, required this.enableNativeBrowser,
required this.textScaleFactor, required this.textScaleFactor,
@ -865,6 +875,8 @@ class _$PrefState extends PrefState {
hideOneYearReviewApp, r'PrefState', 'hideOneYearReviewApp'); hideOneYearReviewApp, r'PrefState', 'hideOneYearReviewApp');
BuiltValueNullFieldError.checkNotNull( BuiltValueNullFieldError.checkNotNull(
hideTwoYearReviewApp, r'PrefState', 'hideTwoYearReviewApp'); hideTwoYearReviewApp, r'PrefState', 'hideTwoYearReviewApp');
BuiltValueNullFieldError.checkNotNull(
hideTaskExtensionBanner, r'PrefState', 'hideTaskExtensionBanner');
BuiltValueNullFieldError.checkNotNull( BuiltValueNullFieldError.checkNotNull(
editAfterSaving, r'PrefState', 'editAfterSaving'); editAfterSaving, r'PrefState', 'editAfterSaving');
BuiltValueNullFieldError.checkNotNull( BuiltValueNullFieldError.checkNotNull(
@ -924,6 +936,7 @@ class _$PrefState extends PrefState {
hideReviewApp == other.hideReviewApp && hideReviewApp == other.hideReviewApp &&
hideOneYearReviewApp == other.hideOneYearReviewApp && hideOneYearReviewApp == other.hideOneYearReviewApp &&
hideTwoYearReviewApp == other.hideTwoYearReviewApp && hideTwoYearReviewApp == other.hideTwoYearReviewApp &&
hideTaskExtensionBanner == other.hideTaskExtensionBanner &&
editAfterSaving == other.editAfterSaving && editAfterSaving == other.editAfterSaving &&
enableNativeBrowser == other.enableNativeBrowser && enableNativeBrowser == other.enableNativeBrowser &&
textScaleFactor == other.textScaleFactor && textScaleFactor == other.textScaleFactor &&
@ -971,6 +984,7 @@ class _$PrefState extends PrefState {
_$hash = $jc(_$hash, hideReviewApp.hashCode); _$hash = $jc(_$hash, hideReviewApp.hashCode);
_$hash = $jc(_$hash, hideOneYearReviewApp.hashCode); _$hash = $jc(_$hash, hideOneYearReviewApp.hashCode);
_$hash = $jc(_$hash, hideTwoYearReviewApp.hashCode); _$hash = $jc(_$hash, hideTwoYearReviewApp.hashCode);
_$hash = $jc(_$hash, hideTaskExtensionBanner.hashCode);
_$hash = $jc(_$hash, editAfterSaving.hashCode); _$hash = $jc(_$hash, editAfterSaving.hashCode);
_$hash = $jc(_$hash, enableNativeBrowser.hashCode); _$hash = $jc(_$hash, enableNativeBrowser.hashCode);
_$hash = $jc(_$hash, textScaleFactor.hashCode); _$hash = $jc(_$hash, textScaleFactor.hashCode);
@ -1018,6 +1032,7 @@ class _$PrefState extends PrefState {
..add('hideReviewApp', hideReviewApp) ..add('hideReviewApp', hideReviewApp)
..add('hideOneYearReviewApp', hideOneYearReviewApp) ..add('hideOneYearReviewApp', hideOneYearReviewApp)
..add('hideTwoYearReviewApp', hideTwoYearReviewApp) ..add('hideTwoYearReviewApp', hideTwoYearReviewApp)
..add('hideTaskExtensionBanner', hideTaskExtensionBanner)
..add('editAfterSaving', editAfterSaving) ..add('editAfterSaving', editAfterSaving)
..add('enableNativeBrowser', enableNativeBrowser) ..add('enableNativeBrowser', enableNativeBrowser)
..add('textScaleFactor', textScaleFactor) ..add('textScaleFactor', textScaleFactor)
@ -1199,6 +1214,11 @@ class PrefStateBuilder implements Builder<PrefState, PrefStateBuilder> {
set hideTwoYearReviewApp(bool? hideTwoYearReviewApp) => set hideTwoYearReviewApp(bool? hideTwoYearReviewApp) =>
_$this._hideTwoYearReviewApp = hideTwoYearReviewApp; _$this._hideTwoYearReviewApp = hideTwoYearReviewApp;
bool? _hideTaskExtensionBanner;
bool? get hideTaskExtensionBanner => _$this._hideTaskExtensionBanner;
set hideTaskExtensionBanner(bool? hideTaskExtensionBanner) =>
_$this._hideTaskExtensionBanner = hideTaskExtensionBanner;
bool? _editAfterSaving; bool? _editAfterSaving;
bool? get editAfterSaving => _$this._editAfterSaving; bool? get editAfterSaving => _$this._editAfterSaving;
set editAfterSaving(bool? editAfterSaving) => set editAfterSaving(bool? editAfterSaving) =>
@ -1272,6 +1292,7 @@ class PrefStateBuilder implements Builder<PrefState, PrefStateBuilder> {
_hideReviewApp = $v.hideReviewApp; _hideReviewApp = $v.hideReviewApp;
_hideOneYearReviewApp = $v.hideOneYearReviewApp; _hideOneYearReviewApp = $v.hideOneYearReviewApp;
_hideTwoYearReviewApp = $v.hideTwoYearReviewApp; _hideTwoYearReviewApp = $v.hideTwoYearReviewApp;
_hideTaskExtensionBanner = $v.hideTaskExtensionBanner;
_editAfterSaving = $v.editAfterSaving; _editAfterSaving = $v.editAfterSaving;
_enableNativeBrowser = $v.enableNativeBrowser; _enableNativeBrowser = $v.enableNativeBrowser;
_textScaleFactor = $v.textScaleFactor; _textScaleFactor = $v.textScaleFactor;
@ -1344,6 +1365,7 @@ class PrefStateBuilder implements Builder<PrefState, PrefStateBuilder> {
hideReviewApp: BuiltValueNullFieldError.checkNotNull(hideReviewApp, r'PrefState', 'hideReviewApp'), hideReviewApp: BuiltValueNullFieldError.checkNotNull(hideReviewApp, r'PrefState', 'hideReviewApp'),
hideOneYearReviewApp: BuiltValueNullFieldError.checkNotNull(hideOneYearReviewApp, r'PrefState', 'hideOneYearReviewApp'), hideOneYearReviewApp: BuiltValueNullFieldError.checkNotNull(hideOneYearReviewApp, r'PrefState', 'hideOneYearReviewApp'),
hideTwoYearReviewApp: BuiltValueNullFieldError.checkNotNull(hideTwoYearReviewApp, r'PrefState', 'hideTwoYearReviewApp'), hideTwoYearReviewApp: BuiltValueNullFieldError.checkNotNull(hideTwoYearReviewApp, r'PrefState', 'hideTwoYearReviewApp'),
hideTaskExtensionBanner: BuiltValueNullFieldError.checkNotNull(hideTaskExtensionBanner, r'PrefState', 'hideTaskExtensionBanner'),
editAfterSaving: BuiltValueNullFieldError.checkNotNull(editAfterSaving, r'PrefState', 'editAfterSaving'), editAfterSaving: BuiltValueNullFieldError.checkNotNull(editAfterSaving, r'PrefState', 'editAfterSaving'),
enableNativeBrowser: BuiltValueNullFieldError.checkNotNull(enableNativeBrowser, r'PrefState', 'enableNativeBrowser'), enableNativeBrowser: BuiltValueNullFieldError.checkNotNull(enableNativeBrowser, r'PrefState', 'enableNativeBrowser'),
textScaleFactor: BuiltValueNullFieldError.checkNotNull(textScaleFactor, r'PrefState', 'textScaleFactor'), textScaleFactor: BuiltValueNullFieldError.checkNotNull(textScaleFactor, r'PrefState', 'textScaleFactor'),

View File

@ -1,9 +1,11 @@
// Flutter imports: // Flutter imports:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// Package imports: // Package imports:
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/redux/task_status/task_status_selectors.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'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
// Project imports: // 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/icons.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart';
import 'package:url_launcher/url_launcher.dart';
class TaskScreen extends StatelessWidget { class TaskScreen extends StatelessWidget {
const TaskScreen({ const TaskScreen({
@ -40,18 +43,18 @@ class TaskScreen extends StatelessWidget {
final state = store.state; final state = store.state;
final company = store.state.company; final company = store.state.company;
final userCompany = store.state.userCompany; final userCompany = store.state.userCompany;
final localization = AppLocalization.of(context); final localization = AppLocalization.of(context)!;
final statuses = [ final statuses = [
TaskStatusEntity().rebuild((b) => b TaskStatusEntity().rebuild((b) => b
..id = kTaskStatusLogged ..id = kTaskStatusLogged
..name = localization!.logged), ..name = localization.logged),
TaskStatusEntity().rebuild((b) => b TaskStatusEntity().rebuild((b) => b
..id = kTaskStatusRunning ..id = kTaskStatusRunning
..name = localization!.running), ..name = localization.running),
if (!state.prefState.showKanban) if (!state.prefState.showKanban)
TaskStatusEntity().rebuild((b) => b TaskStatusEntity().rebuild((b) => b
..id = kTaskStatusInvoiced ..id = kTaskStatusInvoiced
..name = localization!.invoiced), ..name = localization.invoiced),
for (var statusId in memoizedSortedActiveTaskStatusIds( for (var statusId in memoizedSortedActiveTaskStatusIds(
state.taskStatusState.list, state.taskStatusState.map)) state.taskStatusState.list, state.taskStatusState.map))
TaskStatusEntity().rebuild((b) => b TaskStatusEntity().rebuild((b) => b
@ -99,8 +102,57 @@ class TaskScreen extends StatelessWidget {
}, },
) )
], ],
body: body: Column(children: [
state.prefState.showKanban ? KanbanViewBuilder() : TaskListBuilder(), // 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( bottomNavigationBar: AppBottomBar(
entityType: EntityType.task, entityType: EntityType.task,
iconButtons: [ iconButtons: [
@ -166,7 +218,7 @@ class TaskScreen extends StatelessWidget {
Icons.add, Icons.add,
color: Colors.white, color: Colors.white,
), ),
tooltip: localization!.newTask, tooltip: localization.newTask,
) )
: null, : null,
); );

View File

@ -18,6 +18,9 @@ mixin LocalizationsProvider on LocaleCodeAware {
static final Map<String, Map<String, String>> _localizedValues = { static final Map<String, Map<String, String>> _localizedValues = {
'en': { 'en': {
// STARTER: lang key - do not remove comment // 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', 'reactivate_email': 'Reactivate Email',
'email_reactivated': 'Successfully reactivated email', 'email_reactivated': 'Successfully reactivated email',
'template_help': 'Enable using the design as a template', 'template_help': 'Enable using the design as a template',
@ -109980,6 +109983,18 @@ mixin LocalizationsProvider on LocaleCodeAware {
_localizedValues[localeCode]!['email_reactivated'] ?? _localizedValues[localeCode]!['email_reactivated'] ??
_localizedValues['en']!['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 // STARTER: lang field - do not remove comment
String lookup(String? key) { String lookup(String? key) {

View File

@ -191,6 +191,13 @@ String getPlatformName() {
return 'Unknown'; return 'Unknown';
} }
bool isChrome() {
String userAgent = WebUtils.getHtmlValue('user-agent') ?? '';
userAgent = userAgent.toLowerCase();
return userAgent.contains('chrome');
}
String getNativePlatform() { String getNativePlatform() {
String userAgent = WebUtils.getHtmlValue('user-agent') ?? ''; String userAgent = WebUtils.getHtmlValue('user-agent') ?? '';
userAgent = userAgent.toLowerCase(); userAgent = userAgent.toLowerCase();