Skip to content

Commit

Permalink
Merge branch 'develop' into tests/onboarding-goldens
Browse files Browse the repository at this point in the history
  • Loading branch information
JvnSlv committed Feb 3, 2025
2 parents f69f293 + f3e0d8a commit ae67bd5
Show file tree
Hide file tree
Showing 18 changed files with 187 additions and 269 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
@@ -1,13 +1,7 @@
{{> licence.dart }}

import 'dart:convert';
import 'dart:io';

import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart' as http;
import 'package:shelf/shelf.dart';

import '../config.dart';
import '../repositories/push_token_repository.dart';
import '../utils/api_controller.dart';
import '../utils/server_exceptions.dart';
Expand All @@ -30,11 +24,6 @@ class PushNotificationsController extends ApiController {
'/api/user/push-notification-subscriptions',
_unregisterPushHandler,
);
router.addRequest(
RequestType.POST,
'/api/send-push-message',
_broadcastPushHandler,
);
}

Future<Response> _registerPushHandler(Request request) async {
Expand Down Expand Up @@ -64,99 +53,4 @@ class PushNotificationsController extends ApiController {

return responseBuilder.buildOK();
}

Future<Response> _broadcastPushHandler(Request request) async {
final params = await request.bodyFromFormData();
final title = params['title'];
final message = params['message'];
final data = params['data'];
final pushToken = params['pushToken'];

final delayParam = params['delay'];
final delay = delayParam != null && (delayParam is int)
? delayParam
: int.parse(delayParam ?? '0');

throwIfEmpty(
message,
BadRequestException('Push message can not be empty.'),
);
if (!(_pushTokens.tokens.any((element) => element.token == pushToken))) {
throw NotFoundException('Notifications disabled by the user');
}
final accessToken = await _getAccessToken();
for (var token in _pushTokens.tokens) {
Future.delayed(
Duration(seconds: delay),
() async => _sendMessage(
accessToken: accessToken,
title: title,
message: message,
data: data,
pushToken: token.token,
),
);
}

return responseBuilder.buildOK();
}

Future<void> _sendMessage({
required String accessToken,
String? title,
String message = '',
Map<String, Object?>? data,
bool logMessage = true,
String? pushToken,
}) async {
try {
final res = await http.post(
Uri.parse(
'https://fcm.googleapis.com/v1/projects/$projectId/messages:send'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer $accessToken',
},
body: jsonEncode({
'message': {
'token': pushToken,
// "topic": topic, // Use this for a topic
'notification': {
'title': title ?? 'Hello world!',
'body': message,
},
'data': data ?? {},
},
}),
);
if (logMessage) {
print(
'Notification sent: StatusCode: ${res.statusCode} ResponseBody: ${res.body}');
}
} catch (e) {
throw ServerException('Error sending push notification');
}
}

Future<String> _getAccessToken() async {
try {
//the scope url for the firebase messaging
String firebaseMessagingScope =
'https://www.googleapis.com/auth/firebase.messaging';

//get the service account from the json file
final serviceAccount =
json.decode(await File(serviceAccountKeyPath).readAsString());
final client = await clientViaServiceAccount(
ServiceAccountCredentials.fromJson(serviceAccount),
[firebaseMessagingScope]);

final accessToken = client.credentials.accessToken.data;
client.close();
return accessToken;
} catch (_) {
//handle your error here
throw Exception('Error getting access token');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ List<String> securedRoutes = [
'api/count',
'api/count/increment',
'api/count/decrement',
'api/user/push-notification-subscriptions',
'api/send-push-message',{{#enable_mfa}}
'api/user/push-notification-subscriptions',{{#enable_mfa}}
'api/mfa/actions/<action>',
'api/mfa/<transactionId>'{{/enable_mfa}}
];
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 @@ -22,7 +22,4 @@ abstract class PushNotificationsDataSource {
Future<void> unsubscribePushToken(
@Body() PushNotificationDataRequestModel pushToken,
);

@POST('/api/send-push-message')
Future<void> sendPushMessage(@Body() PushMessageRequestModel message);
}
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,6 +28,8 @@ class DesignSystemIcons {

final send = Icons.send;

final copy = Icons.copy;

final success = Icons.check_circle_outline;

final dashboardOutlined = Icons.dashboard_outlined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,45 @@
import 'package:rx_bloc/rx_bloc.dart';
import 'package:rxdart/rxdart.dart';

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

part 'notifications_bloc.rxb.g.dart';

/// A contract class containing all events of the NotificationsBloC.
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});
// Fetch notifications provider push token
void fetchPushToken();
}

/// 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;

/// The error state
Stream<ErrorModel> get errors;
}

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

final NotificationService _service;

@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();
Stream<ErrorModel> _mapToErrorsState() => errorState.mapToErrorModel();

@override
ConnectableStream<Result<String>> _mapToPushTokenState() =>
_$fetchPushTokenEvent
.startWith(null)
.switchMap(
(_) => _service.getPushToken().asResultStream(),
)
.setResultStateHandler(this)
.publish();
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
{{> licence.dart }}

import '../../base/models/errors/error_model.dart';
import '../../base/repositories/push_notification_repository.dart';

class NotificationService {
NotificationService(this._repository);

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() async =>
await _repository.getToken() ?? (throw NotFoundErrorModel());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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!));
}
},
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,
child: Icon(context.designSystem.icons.copy),
),
),
),
],
),
),
),
);
}
Loading

0 comments on commit ae67bd5

Please sign in to comment.