Skip to content

Commit

Permalink
Push notifications showcase improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
nikolay-vasilev-prime committed Jan 28, 2025
1 parent e0029cf commit 0b11dfa
Show file tree
Hide file tree
Showing 14 changed files with 182 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,12 @@ class ShowcasePage extends StatelessWidget {
extension on BuildContext {
List<({String title, String subtitle, RouteDataModel route, Icon icon})>
get features => [
// TODO: Removed until: rx_bloc#929 or rx_bloc#941 is resolved
// (
// title: l10n.featureNotifications.notificationPageTitle,
// subtitle: l10n.featureNotifications.notificationPageSubtitle,
// route: const NotificationsRoute(),
// icon: designSystem.icons.notifications,
// ),
(
title: l10n.featureNotifications.notificationPageTitle,
subtitle: l10n.featureNotifications.notificationPageSubtitle,
route: const NotificationsRoute(),
icon: designSystem.icons.notifications,
),
{{#enable_feature_counter}}(
title: l10n.featureShowcase.counterShowcase,
subtitle: l10n.featureShowcase.counterShowcaseDescription,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ const String webVapidKey = '';

/// The duration to debounce actions to prevent multiple actions from being
/// triggered in a short period of time.
const actionDebounceDuration = Duration(milliseconds: 500);
const actionDebounceDuration = Duration(milliseconds: 500);

const String firebaseProjectUrl = 'https://console.firebase.google.com/';
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,6 @@ class PushNotificationRepository {
final FirebaseMessaging _firebaseMessaging;
final NotificationsLocalDataSource _localDataSource;

// Sends a push notification to the server which will be broadcast to all
// logged in users.
Future<void> sendPushMessage({
required String message,
String? title,
int? delay,
Map<String, Object?>? data,
}) async {
final pushToken = await getToken();
return _errorMapper.execute(
() => _pushDataSource.sendPushMessage(
PushMessageRequestModel(
message: message,
title: title,
delay: delay ?? 0,
data: data ?? {},
pushToken: pushToken,
),
),
);
}

// Checks if the user has granted permissions for displaying push messages.
// If called the very first time, the user is asked to grant permissions.
Future<bool> requestNotificationPermissions() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ class DesignSystemIcons {

final send = Icons.send;

final copy = Icons.copy;

final success = Icons.check_circle_outline;

final dashboardOutlined = Icons.dashboard_outlined;
final dashboardOutlined = Icons.dashboard_outlined;

final Icon calculateIcon = _getIcon(Icons.calculate);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import 'package:rx_bloc/rx_bloc.dart';
import 'package:rxdart/rxdart.dart';

import '../../base/models/errors/error_model.dart';
import '../services/notifications_service.dart';

part 'notifications_bloc.rxb.g.dart';
Expand All @@ -11,39 +12,41 @@ part 'notifications_bloc.rxb.g.dart';
abstract class NotificationsBlocEvents {
/// Requests permissions for displaying push notifications
void requestNotificationPermissions();

/// Issues a new push message
void sendMessage(String message,
{String? title, int? delay, Map<String, Object?>? data});
}

/// A contract class containing all states of the NotificationsBloC.
abstract class NotificationsBlocStates {
/// Are the permissions for displaying push notifications granted
Stream<bool> get permissionsAuthorized;

/// The push token to which the developers can send notifications
ConnectableStream<Result<String>> get pushToken;
}

@RxBloc()
class NotificationsBloc extends $NotificationsBloc {
NotificationsBloc(this._service);
NotificationsBloc(this._service) {
pushToken.connect().addTo(_compositeSubscription);
}

final NotificationService _service;


@override
Stream<bool> _mapToPermissionsAuthorizedState() =>
_$requestNotificationPermissionsEvent
.switchMap(
(_) => _service.requestNotificationPermissions().asResultStream(),
)
.setResultStateHandler(this)
.whereSuccess();

@override
Stream<bool> _mapToPermissionsAuthorizedState() => Rx.merge([
_$sendMessageEvent.switchMap(
(args) => _service
.sendPushMessage(
message: args.message,
title: args.title,
delay: args.delay,
data: args.data,
)
.then((_) => true)
.asResultStream(),
),
_$requestNotificationPermissionsEvent.switchMap(
(_) => _service.requestNotificationPermissions().asResultStream(),
),
]).setResultStateHandler(this).whereSuccess();
ConnectableStream<Result<String>> _mapToPushTokenState() =>
PublishSubject<String>()
.startWith('')
.switchMap((_) => _service.getPushToken().asResultStream())
.setResultStateHandler(this)
.mapResult((token) => token ?? (throw NotFoundErrorModel()))
.publish();
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,9 @@ class NotificationService {

final PushNotificationRepository _repository;

Future<void> sendPushMessage({
required String message,
String? title,
int? delay,
Map<String, Object?>? data,
}) =>
_repository.sendPushMessage(
message: message,
title: title,
delay: delay,
data: data,
);

Future<bool> requestNotificationPermissions() =>
_repository.requestNotificationPermissions();

Future<String?> getPushToken() =>
_repository.getToken();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:widget_toolkit/shimmer.dart';
import 'package:widget_toolkit/text_field_dialog.dart';

import '../../app_extensions.dart';

class PushTokenWidget extends StatelessWidget {
const PushTokenWidget(
{required this.label, this.value, this.error, super.key});

final String label;
final String? value;
final String? error;

@override
Widget build(BuildContext context) => InkWell(
onTap: () {
if (value != null) {
Clipboard.setData(ClipboardData(text: value!)).then((_) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.copiedToYourKeyboard)));
}
});
}
},
customBorder: const CircleBorder(),
child: Container(
decoration: BoxDecoration(
color: context.textFieldDialogTheme.editFieldRegularBackground,
borderRadius: BorderRadius.circular(
context.textFieldDialogTheme.editFieldBorderRadius,
),
),
child: Padding(
padding: EdgeInsets.symmetric(
vertical: context.textFieldDialogTheme.spacingS,
horizontal: context.textFieldDialogTheme.spacingM,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: context.textFieldDialogTheme.captionBold
.copyWith(
color: context.textFieldDialogTheme
.editFieldLabelNotEditedColor),
),
SizedBox(height: context.textFieldDialogTheme.spacingXSS),
ShimmerText(
error ?? value,
style: context.textFieldDialogTheme
.editFieldTextNotEditedTextStyle
.copyWith(
color: error == null
? context.textFieldDialogTheme
.editFieldValueNotEditedColor
: context.designSystem.colors.errorColor),
)
],
),
),
Visibility(
visible: value != null,
replacement: SizedBox(),
child: Padding(
padding: EdgeInsets.only(
left: context.textFieldDialogTheme.spacingS,
top: context.textFieldDialogTheme.spacingXS,
bottom: context.textFieldDialogTheme.spacingXS),
child: Material(
color: Colors.transparent,
//.copyWith(color: _getValueColor(context));
child: Icon(context.designSystem.icons.copy),
),
),
),
],
),
),
),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rx_bloc/flutter_rx_bloc.dart';
import 'package:provider/provider.dart';
import 'package:widget_toolkit/ui_components.dart';
import 'package:widget_toolkit/widget_toolkit.dart';

import '../../app_extensions.dart';
import '../../base/app/config/app_constants.dart';
import '../../base/common_ui_components/app_list_tile.dart';
import '../../base/common_ui_components/custom_app_bar.dart';
import '../../base/models/notification_model.dart';
import '../../lib_router/router.dart';
import '../blocs/notifications_bloc.dart';
import '../ui_components/push_token_widget.dart';

class NotificationsPage extends StatelessWidget {
const NotificationsPage({super.key});
Expand Down Expand Up @@ -69,47 +70,46 @@ class NotificationsPage extends StatelessWidget {
.events
.requestNotificationPermissions(),
),
AppListTile(
featureTitle:
context.l10n.featureNotifications.notificationShowText,
trailing: const SizedBox(),
icon: const Icon(Icons.notifications_active_outlined),
onTap: () => context
.read<NotificationsBlocType>()
.events
.sendMessage(context
.l10n.featureNotifications.notificationsMessage),
SizedBox(
height: context.designSystem.spacing.m,
),
AppListTile(
featureTitle: context
.l10n.featureNotifications.notificationShowDelayedText,
trailing: const SizedBox(),
icon: const Icon(Icons.notifications_paused_outlined),
onTap: () => context
.read<NotificationsBlocType>()
.events
.sendMessage(
context.l10n.featureNotifications.notificationsDelayed,
delay: 5,
),
Padding(
padding: EdgeInsets.only(
left: context.designSystem.spacing.l,
right: context.designSystem.spacing.l,
),
AppListTile(
featureTitle: context.l10n.featureNotifications
.notificationShowRedirectingText,
trailing: const SizedBox(),
icon: const Icon(Icons.circle_notifications_outlined),
onTap: () => context
.read<NotificationsBlocType>()
.events
.sendMessage(
context
.l10n.featureNotifications.notificationRedirecing,
delay: 5,
data: NotificationModel(
type: NotificationModelType.dashboard,
id: '1',
).toJson()),
child: PushTokenWidget(
label: context
.l10n.featureNotifications.notificationConsoleLabel,
value: firebaseProjectUrl),
),
SizedBox(
height: context.designSystem.spacing.l,
),
Padding(
padding: EdgeInsets.only(
left: context.designSystem.spacing.l,
right: context.designSystem.spacing.l,
),
child: RxResultBuilder<NotificationsBlocType, String>(
state: (bloc) => bloc.states.pushToken,
buildSuccess: (context, pushToken, bloc) => PushTokenWidget(
label: context
.l10n.featureNotifications.notificationTokenLabel,
value: pushToken,
),
buildError: (context, error, bloc) => PushTokenWidget(
label: context
.l10n.featureNotifications.notificationTokenLabel,
error: context.l10n.error.notImplemented,
),
buildLoading: (context, bloc) => PushTokenWidget(
label: context
.l10n.featureNotifications.notificationTokenLabel,
value: null,
),
),
),
RxBlocListener<NotificationsBlocType, bool>(
state: (bloc) => bloc.states.permissionsAuthorized,
listener: (ctx, authorized) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
"english": "Английски",
"bulgarian": "Български",
"changeLanguage" : "Смени език",
"navShowcase":"Страница с демонстрации"
"navShowcase":"Страница с демонстрации",
"copiedToYourKeyboard":"Стойността беше копирана във вашия клипборд"
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
"english" : "English",
"bulgarian" : "Bulgarian",
"changeLanguage" : "Change Language",
"navShowcase":"Showcase"
"navShowcase":"Showcase",
"copiedToYourKeyboard":"Copied to your clipboard !"
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
"tooShort": "Въведеният текст е твърде кратък",
"notificationsDisabledMessage":"Изглежда, че сте отказали известия на това устройство. За да можем да показваме известия, те трябва да бъдат активирани ръчно от настройките на устройството.",
"noMailApp": "Нямате пощенски клиент на устройството. Моля инсталирайте или отворете с браузър.",
"invalidUrl": "Невалиден URL адрес"
"invalidUrl": "Невалиден URL адрес",
"notImplemented": "Тази функционалност не е завършена"
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
"tooShort": "The entered text is too short",
"notificationsDisabledMessage":"Seems that you have denied notifications on this device. In order for us to show notifications, they need to be manually enabled from the device settings.",
"noMailApp": "No e-mail client found on your device. Please, install one or open your mail provider with a browser.",
"invalidUrl": "Invalid URL"
"invalidUrl": "Invalid URL",
"notImplemented": "This feature is not implemented"
}
Loading

0 comments on commit 0b11dfa

Please sign in to comment.