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 =
'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';

View File

@ -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});

View File

@ -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<bool> hideDesktopWarningReducer = combineReducers([
}),
]);
Reducer<bool> hideTaskExtensionBannerReducer = combineReducers([
TypedReducer<bool, DismissTaskExtensionBanner>((filter, action) {
return true;
}),
]);
Reducer<bool> hideGatewayWarningReducer = combineReducers([
TypedReducer<bool, DismissGatewayWarningPermanently>((filter, action) {
return true;

View File

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

View File

@ -220,6 +220,9 @@ class _$PrefStateSerializer implements StructuredSerializer<PrefState> {
'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<PrefState> {
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<PrefState, PrefStateBuilder> {
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<PrefState, PrefStateBuilder> {
_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<PrefState, PrefStateBuilder> {
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'),

View File

@ -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,
);

View File

@ -18,6 +18,9 @@ mixin LocalizationsProvider on LocaleCodeAware {
static final Map<String, Map<String, String>> _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) {

View File

@ -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();