feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,16 @@
// Copyright 2021-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/EXExportedModule.h>
#import <ExpoModulesCore/EXModuleRegistryConsumer.h>
#import <EXNotifications/EXNotificationsDelegate.h>
typedef NS_ENUM(NSUInteger, EXBackgroundNotificationResult) {
EXBackgroundNotificationResultNoData = 1,
EXBackgroundNotificationResultNewData = 2,
EXBackgroundNotificationResultFailed = 3,
};
@interface EXBackgroundNotificationTasksModule : EXExportedModule <EXModuleRegistryConsumer>
@end

View File

@@ -0,0 +1,62 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <EXNotifications/EXBackgroundNotificationTasksModule.h>
#import <EXNotifications/EXBackgroundRemoteNotificationConsumer.h>
#import <ExpoModulesCore/EXTaskManagerInterface.h>
@interface EXBackgroundNotificationTasksModule ()
@property (nonatomic, weak) id<EXTaskManagerInterface> taskManager;
@end
@implementation EXBackgroundNotificationTasksModule
EX_EXPORT_MODULE(ExpoBackgroundNotificationTasksModule);
# pragma mark - EXModuleRegistryConsumer
- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry
{
_taskManager = [moduleRegistry getModuleImplementingProtocol:@protocol(EXTaskManagerInterface)];
}
# pragma mark - Exported methods
EX_EXPORT_METHOD_AS(registerTaskAsync,
registerTaskWithName:(nonnull NSString *)taskName
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
if (![_taskManager hasBackgroundModeEnabled:@"remote-notification"]) {
return reject(
@"E_BACKGROUND_REMOTE_NOTIFICATIONS_DISABLED",
@"Background remote notifications have not been configured. To enable it, add `remote-notification` to `UIBackgroundModes` in the application's Info.plist file.",
nil
);
}
@try {
[_taskManager registerTaskWithName:taskName
consumer:EXBackgroundRemoteNotificationConsumer.class
options:@{}];
}
@catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
EX_EXPORT_METHOD_AS(unregisterTaskAsync,
unregisterTaskWithName:(nonnull NSString *)taskName
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
@try {
[_taskManager unregisterTaskWithName:taskName consumerClass:[EXBackgroundRemoteNotificationConsumer class]];
} @catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
@end

View File

@@ -0,0 +1,14 @@
// Copyright 2021-present 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
#import <ExpoModulesCore/EXTaskConsumerInterface.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXBackgroundRemoteNotificationConsumer : NSObject <EXTaskConsumerInterface>
@property (nonatomic, strong) id<EXTaskInterface> task;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,51 @@
// Copyright 2021-present 650 Industries. All rights reserved.
#import <EXNotifications/EXBackgroundNotificationTasksModule.h>
#import <EXNotifications/EXBackgroundRemoteNotificationConsumer.h>
#import <ExpoModulesCore/EXTaskInterface.h>
@implementation EXBackgroundRemoteNotificationConsumer
+ (BOOL)supportsLaunchReason:(EXTaskLaunchReason)launchReason
{
return launchReason == EXTaskLaunchReasonRemoteNotification;
}
- (NSString *)taskType
{
return @"remote-notification";
}
// Associating task to the consumer.
- (void)didRegisterTask:(id<EXTaskInterface>)task
{
_task = task;
}
// Method that is being called when the JS app just finished launching,
// after the native app was launched with the launch reason supported by the consumer.
// For background notifications, `application:didReceiveRemoteNotification:fetchCompletionHandler:` is the entry point of this method,
// so the task can be executed immediately here
- (void)didBecomeReadyToExecuteWithData:(NSDictionary *)data
{
[_task executeWithData:data withError:nil];
}
// Translate result received from JS to another (native) type that is then used for example as an argument in completion callbacks.
- (NSUInteger)normalizeTaskResult:(id)result
{
if (!result || result == [NSNull null]) {
return UIBackgroundFetchResultNoData;
}
switch ([result unsignedIntegerValue]) {
case EXBackgroundNotificationResultNewData:
return UIBackgroundFetchResultNewData;
case EXBackgroundNotificationResultFailed:
return UIBackgroundFetchResultFailed;
case EXBackgroundNotificationResultNoData:
default:
return UIBackgroundFetchResultNoData;
}
}
@end

View File

@@ -0,0 +1,25 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/EXExportedModule.h>
#import <EXNotifications/EXNotificationsDelegate.h>
@interface EXNotificationCategoriesModule : EXExportedModule <EXNotificationsDelegate>
- (void)getNotificationCategoriesAsyncWithResolver:(EXPromiseResolveBlock)resolve reject:(EXPromiseRejectBlock)reject;
- (void)setNotificationCategoryWithCategoryId:(NSString *)categoryId
actions:(NSArray *)actions
options:(NSDictionary *)options
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject;
- (void)deleteNotificationCategoryWithCategoryId:(NSString *)categoryId
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject;
- (NSMutableDictionary *)serializeCategory:(UNNotificationCategory *)category;
- (UNNotificationCategory *)createCategoryWithId:(NSString*)categoryId
actions:(NSArray *)actions
options:(NSDictionary *)options;
- (NSMutableDictionary *)serializeCategoryOptions:(UNNotificationCategory *)category;
- (NSMutableArray *)serializeActions:(NSArray<UNNotificationAction *>*)actions;
- (NSMutableDictionary *)serializeActionOptions:(NSUInteger)options;
@end

View File

@@ -0,0 +1,217 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <EXNotifications/EXNotificationCategoriesModule.h>
#import <EXNotifications/EXNotificationCenterDelegate.h>
@implementation EXNotificationCategoriesModule
EX_EXPORT_MODULE(ExpoNotificationCategoriesModule);
# pragma mark - Exported methods
EX_EXPORT_METHOD_AS(getNotificationCategoriesAsync,
getNotificationCategoriesAsyncWithResolver:(EXPromiseResolveBlock)resolve reject:(EXPromiseRejectBlock)reject)
{
[[UNUserNotificationCenter currentNotificationCenter] getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *categories) {
NSMutableArray* existingCategories = [NSMutableArray new];
for (UNNotificationCategory *category in categories) {
[existingCategories addObject:[self serializeCategory:category]];
}
resolve(existingCategories);
}];
}
EX_EXPORT_METHOD_AS(setNotificationCategoryAsync,
setNotificationCategoryWithCategoryId:(NSString *)categoryId
actions:(NSArray *)actions
options:(NSDictionary *)options
resolve:(EXPromiseResolveBlock)resolve reject:(EXPromiseRejectBlock)reject)
{
UNNotificationCategory *newCategory = [EXNotificationCategoriesModule createCategoryWithId:categoryId
actions:actions
options:options];
[[UNUserNotificationCenter currentNotificationCenter] getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *categories) {
NSMutableSet<UNNotificationCategory *> *newCategories = [categories mutableCopy] ?: [[NSMutableSet alloc] init];
for (UNNotificationCategory *category in newCategories) {
if ([category.identifier isEqualToString:newCategory.identifier]) {
[newCategories removeObject:category];
break;
}
}
[newCategories addObject:newCategory];
[[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:newCategories];
resolve([self serializeCategory:newCategory]);
}];
}
EX_EXPORT_METHOD_AS(deleteNotificationCategoryAsync,
deleteNotificationCategoryWithCategoryId:(NSString *)categoryId
resolve:(EXPromiseResolveBlock)resolve reject:(EXPromiseRejectBlock)reject)
{
[[UNUserNotificationCenter currentNotificationCenter] getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *categories) {
BOOL didDelete = NO;
NSMutableSet<UNNotificationCategory *> *newCategories = [categories mutableCopy];
for (UNNotificationCategory *category in newCategories) {
if ([category.identifier isEqualToString:categoryId]) {
[newCategories removeObject:category];
didDelete = YES;
break;
}
}
[[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:newCategories];
resolve(@(didDelete));
}];
}
# pragma mark- Internal
+ (UNNotificationCategory *)createCategoryWithId:(NSString*)categoryId
actions:(NSArray *)actions
options:(NSDictionary *)options
{
NSArray<NSString *> *intentIdentifiers = options[@"intentIdentifiers"];
NSString *previewPlaceholder = options[@"previewPlaceholder"];
NSString *categorySummaryFormat = options[@"categorySummaryFormat"];
NSMutableArray<UNNotificationAction *> *actionsArray = [[NSMutableArray alloc] init];
for (NSDictionary<NSString *, id> *actionParams in actions) {
[actionsArray addObject:[self parseNotificationActionFromParams:actionParams]];
}
UNNotificationCategoryOptions categoryOptions = [self parseNotificationCategoryOptionsFromParams: options];
UNNotificationCategory *newCategory;
if (@available(iOS 12, *)) {
newCategory = [UNNotificationCategory categoryWithIdentifier:categoryId
actions:actionsArray
intentIdentifiers:intentIdentifiers
hiddenPreviewsBodyPlaceholder:previewPlaceholder
categorySummaryFormat:categorySummaryFormat
options:categoryOptions];
} else if (@available(iOS 11, *)) {
newCategory = [UNNotificationCategory categoryWithIdentifier:categoryId
actions:actionsArray
intentIdentifiers:intentIdentifiers
hiddenPreviewsBodyPlaceholder:previewPlaceholder
options:categoryOptions];
} else {
newCategory = [UNNotificationCategory categoryWithIdentifier:categoryId
actions:actionsArray
intentIdentifiers:intentIdentifiers
options:categoryOptions];
}
return newCategory;
}
+ (UNNotificationAction *)parseNotificationActionFromParams:(NSDictionary *)params
{
NSString *identifier = params[@"identifier"];
NSString *buttonTitle = params[@"buttonTitle"];
UNNotificationActionOptions options = UNNotificationActionOptionNone;
if (params[@"options"][@"opensAppToForeground"] == nil || [params[@"options"][@"opensAppToForeground"] boolValue]) {
options += UNNotificationActionOptionForeground;
}
if ([params[@"options"][@"isDestructive"] boolValue]) {
options += UNNotificationActionOptionDestructive;
}
if ([params[@"options"][@"isAuthenticationRequired"] boolValue]) {
options += UNNotificationActionOptionAuthenticationRequired;
}
if ([params[@"textInput"] isKindOfClass:[NSDictionary class]]) {
return [UNTextInputNotificationAction actionWithIdentifier:identifier
title:buttonTitle
options:options
textInputButtonTitle:params[@"textInput"][@"submitButtonTitle"]
textInputPlaceholder:params[@"textInput"][@"placeholder"]];
}
return [UNNotificationAction actionWithIdentifier:identifier title:buttonTitle options:options];
}
+ (UNNotificationCategoryOptions )parseNotificationCategoryOptionsFromParams:(NSDictionary *)params
{
UNNotificationCategoryOptions options = UNNotificationCategoryOptionNone;
if ([params[@"customDismissAction"] boolValue]) {
options += UNNotificationCategoryOptionCustomDismissAction;
}
if ([params[@"allowInCarPlay"] boolValue]) {
options += UNNotificationCategoryOptionAllowInCarPlay;
}
if (@available(iOS 11, *)) {
if ([params[@"showTitle"] boolValue]) {
options += UNNotificationCategoryOptionHiddenPreviewsShowTitle;
}
if ([params[@"showSubtitle"] boolValue]) {
options += UNNotificationCategoryOptionHiddenPreviewsShowSubtitle;
}
}
if (@available(iOS 13, *)) {
if ([params[@"allowAnnouncement"] boolValue]) {
options += UNNotificationCategoryOptionAllowAnnouncement;
}
}
return options;
}
- (NSMutableDictionary *)serializeCategory:(UNNotificationCategory *)category
{
NSMutableDictionary* serializedCategory = [NSMutableDictionary dictionary];
serializedCategory[@"identifier"] = category.identifier;
serializedCategory[@"actions"] = [self serializeActions: category.actions];
serializedCategory[@"options"] = [self serializeCategoryOptions: category];
return serializedCategory;
}
- (NSMutableDictionary *)serializeCategoryOptions:(UNNotificationCategory *)category
{
NSMutableDictionary* serializedOptions = [NSMutableDictionary dictionary];
serializedOptions[@"intentIdentifiers"] = category.intentIdentifiers;
serializedOptions[@"customDismissAction"] = [NSNumber numberWithBool:((category.options & UNNotificationCategoryOptionCustomDismissAction) != 0)];
serializedOptions[@"allowInCarPlay"] = [NSNumber numberWithBool:((category.options & UNNotificationCategoryOptionAllowInCarPlay) != 0)];
if (@available(iOS 11, *)) {
serializedOptions[@"previewPlaceholder"] = category.hiddenPreviewsBodyPlaceholder;
serializedOptions[@"showTitle"] = [NSNumber numberWithBool:((category.options & UNNotificationCategoryOptionHiddenPreviewsShowTitle) != 0)];
serializedOptions[@"showSubtitle"] = [NSNumber numberWithBool:((category.options & UNNotificationCategoryOptionHiddenPreviewsShowSubtitle) != 0)];
}
if (@available(iOS 12, *)) {
serializedOptions[@"categorySummaryFormat"] = category.categorySummaryFormat;
}
if (@available(iOS 13, *)) {
serializedOptions[@"allowAnnouncement"] = [NSNumber numberWithBool:((category.options & UNNotificationActionOptionAuthenticationRequired) != 0)];
}
return serializedOptions;
}
- (NSMutableArray *)serializeActions:(NSArray<UNNotificationAction *>*)actions
{
NSMutableArray* serializedActions = [NSMutableArray new];
for (NSUInteger i = 0; i < [actions count]; i++)
{
NSMutableDictionary *actionDictionary = [NSMutableDictionary dictionary];
actionDictionary[@"buttonTitle"] = actions[i].title;
actionDictionary[@"identifier"] = actions[i].identifier;
actionDictionary[@"options"] = [self serializeActionOptions:actions[i].options];
if ([actions[i] isKindOfClass:[UNTextInputNotificationAction class]]) {
UNTextInputNotificationAction *textInputAction = (UNTextInputNotificationAction *)actions[i];
NSMutableDictionary *textInputOptions = [NSMutableDictionary dictionary];
textInputOptions[@"placeholder"] = textInputAction.textInputPlaceholder;
textInputOptions[@"submitButtonTitle"] = textInputAction.textInputButtonTitle;
actionDictionary[@"textInput"] = textInputOptions;
}
[serializedActions addObject:actionDictionary];
}
return serializedActions;
}
- (NSMutableDictionary *)serializeActionOptions:(NSUInteger)options
{
NSMutableDictionary* serializedOptions = [NSMutableDictionary dictionary];
serializedOptions[@"opensAppToForeground"] = [NSNumber numberWithBool:((options & UNNotificationActionOptionForeground) != 0)];
serializedOptions[@"isDestructive"] = [NSNumber numberWithBool:((options & UNNotificationActionOptionDestructive) != 0)];
serializedOptions[@"isAuthenticationRequired"] = [NSNumber numberWithBool:((options & UNNotificationActionOptionAuthenticationRequired) != 0)];
return serializedOptions;
}
@end

View File

@@ -0,0 +1,33 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <ExpoModulesCore/EXSingletonModule.h>
#import <UserNotifications/UserNotifications.h>
#import <EXNotifications/EXNotificationsDelegate.h>
NS_ASSUME_NONNULL_BEGIN
@protocol EXNotificationCenterDelegate
- (void)addDelegate:(id<EXNotificationsDelegate>)delegate;
- (void)removeDelegate:(id<EXNotificationsDelegate>)delegate;
- (nullable UNNotificationResponse *)lastNotificationResponse;
- (void)setLastNotificationResponse:(nullable UNNotificationResponse *)response;
@end
@interface EXNotificationCenterDelegate : EXSingletonModule <UIApplicationDelegate, UNUserNotificationCenterDelegate, EXNotificationCenterDelegate>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary<UIApplicationLaunchOptionsKey,id> *)launchOptions;
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler;
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler;
- (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsForNotification:(nullable UNNotification *)notification;
@property (nonatomic, strong, nullable) UNNotificationResponse *lastNotificationResponse;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,211 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <EXNotifications/EXNotificationCenterDelegate.h>
#import <ExpoModulesCore/EXDefines.h>
#import <EXNotifications/EXNotificationsDelegate.h>
@interface EXNotificationCenterDelegate ()
@property (nonatomic, strong) NSPointerArray *delegates;
@property (nonatomic, strong) NSMutableArray<UNNotificationResponse *> *pendingNotificationResponses;
@end
@implementation EXNotificationCenterDelegate
EX_REGISTER_SINGLETON_MODULE(NotificationCenterDelegate);
- (instancetype)init
{
if (self = [super init]) {
_delegates = [NSPointerArray weakObjectsPointerArray];
_pendingNotificationResponses = [NSMutableArray array];
}
return self;
}
# pragma mark - UIApplicationDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey,id> *)launchOptions
{
if ([UNUserNotificationCenter currentNotificationCenter].delegate) {
EXLogWarn(@"[expo-notifications] EXNotificationCenterDelegate encountered already present delegate of UNUserNotificationCenter: %@. "
"EXNotificationCenterDelegate will not overwrite the value not to break other features of your app. "
"In return, expo-notifications may not work properly. "
"To fix this problem either remove setting of the second delegate "
"or set the delegate to an instance of EXNotificationCenterDelegate manually afterwards.",
[UNUserNotificationCenter currentNotificationCenter].delegate);
return YES;
}
[[UNUserNotificationCenter currentNotificationCenter] setDelegate:self];
return YES;
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
__block int delegatesCalled = 0;
__block int delegatesCompleted = 0;
__block BOOL delegatingCompleted = NO;
__block BOOL delegatesFailed = 0;
__block UIBackgroundFetchResult resultSum = UIBackgroundFetchResultNoData;
__block void (^completionHandlerCaller)(void) = ^{
if (delegatingCompleted && delegatesCompleted == delegatesCalled) {
if (delegatesCompleted == delegatesFailed) {
// If all delegates failed to fetch result, let's let the OS know about that
completionHandler(UIBackgroundFetchResultFailed);
} else {
// If at least one succeeded, let's take it as read and respond with that result.
completionHandler(resultSum);
}
}
};
for (int i = 0; i < _delegates.count; i++) {
id pointer = [_delegates pointerAtIndex:i];
if ([pointer respondsToSelector:@selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)]) {
[pointer application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:^(UIBackgroundFetchResult result) {
@synchronized (self) {
if (result == UIBackgroundFetchResultFailed) {
delegatesFailed += 1;
} else if (result == UIBackgroundFetchResultNewData) {
resultSum = UIBackgroundFetchResultNewData;
}
delegatesCompleted += 1;
completionHandlerCaller();
}
}];
@synchronized (self) {
delegatesCalled += 1;
}
}
}
@synchronized (self) {
delegatingCompleted = YES;
completionHandlerCaller();
}
}
# pragma mark - UNUserNotificationCenterDelegate
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{
__block int delegatesCalled = 0;
__block int delegatesCompleted = 0;
__block BOOL delegatingCompleted = NO;
__block UNNotificationPresentationOptions optionsSum = UNNotificationPresentationOptionNone;
__block void (^completionHandlerCaller)(void) = ^{
if (delegatingCompleted && delegatesCompleted == delegatesCalled) {
completionHandler(optionsSum);
}
};
for (int i = 0; i < _delegates.count; i++) {
id pointer = [_delegates pointerAtIndex:i];
if ([pointer respondsToSelector:@selector(userNotificationCenter:willPresentNotification:withCompletionHandler:)]) {
[pointer userNotificationCenter:center willPresentNotification:notification withCompletionHandler:^(UNNotificationPresentationOptions options) {
@synchronized (self) {
delegatesCompleted += 1;
optionsSum = optionsSum | options;
completionHandlerCaller();
}
}];
@synchronized (self) {
delegatesCalled += 1;
}
}
}
@synchronized (self) {
delegatingCompleted = YES;
completionHandlerCaller();
}
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
{
// Save last response here for use by EXNotificationsEmitter
self.lastNotificationResponse = response;
// Save response to pending responses array if none of the handlers will handle it.
BOOL responseWillBeHandledByAppropriateDelegate = NO;
for (int i = 0; i < _delegates.count; i++) {
id pointer = [_delegates pointerAtIndex:i];
if ([pointer respondsToSelector:@selector(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:)]) {
responseWillBeHandledByAppropriateDelegate = YES;
break;
}
}
if (!responseWillBeHandledByAppropriateDelegate) {
[_pendingNotificationResponses addObject:response];
}
__block int delegatesCalled = 0;
__block int delegatesCompleted = 0;
__block BOOL delegatingCompleted = NO;
void (^completionHandlerCaller)(void) = ^{
if (delegatingCompleted && delegatesCompleted == delegatesCalled) {
completionHandler();
}
};
for (int i = 0; i < _delegates.count; i++) {
id pointer = [_delegates pointerAtIndex:i];
if ([pointer respondsToSelector:@selector(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:)]) {
[pointer userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:^{
@synchronized (self) {
delegatesCompleted += 1;
completionHandlerCaller();
}
}];
@synchronized (self) {
delegatesCalled += 1;
}
}
}
@synchronized (self) {
delegatingCompleted = YES;
completionHandlerCaller();
}
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsForNotification:(UNNotification *)notification
{
if (@available(iOS 12.0, *)) {
for (int i = 0; i < _delegates.count; i++) {
id pointer = [_delegates pointerAtIndex:i];
if ([pointer respondsToSelector:@selector(userNotificationCenter:openSettingsForNotification:)]) {
[pointer userNotificationCenter:center openSettingsForNotification:notification];
}
}
}
}
# pragma mark - EXNotificationCenterDelegate
- (void)addDelegate:(id<EXNotificationsDelegate>)delegate
{
[_delegates addPointer:(__bridge void * _Nullable)(delegate)];
if ([delegate respondsToSelector:@selector(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:)]) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
for (UNNotificationResponse *response in _pendingNotificationResponses) {
[delegate userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:^{
// completion handler doesn't need to do anything
}];
}
}
}
- (void)removeDelegate:(id<EXNotificationsDelegate>)delegate
{
for (int i = 0; i < _delegates.count; i++) {
id pointer = [_delegates pointerAtIndex:i];
if (pointer == (__bridge void * _Nullable)(delegate) || !pointer) {
[_delegates removePointerAtIndex:i];
i--;
}
}
// compact doesn't work, that's why we need the `|| !pointer` above
// http://www.openradar.me/15396578
[_delegates compact];
}
@end

View File

@@ -0,0 +1,16 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <UserNotifications/UserNotifications.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXNotificationSerializer : NSObject
+ (NSDictionary *)serializedNotification:(UNNotification *)notification;
+ (NSDictionary *)serializedNotificationRequest:(UNNotificationRequest *)notificationRequest;
+ (NSDictionary *)serializedNotificationResponse:(UNNotificationResponse *)notificationResponse;
+ (NSDictionary *)serializedNotificationContent:(UNNotificationRequest *)request;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,249 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <EXNotifications/EXNotificationSerializer.h>
#import <CoreLocation/CoreLocation.h>
NS_ASSUME_NONNULL_BEGIN
static NSString * const EXNotificationResponseDefaultActionIdentifier = @"expo.modules.notifications.actions.DEFAULT";
@implementation EXNotificationSerializer
+ (NSDictionary *)serializedNotificationResponse:(UNNotificationResponse *)response
{
NSMutableDictionary *serializedResponse = [NSMutableDictionary dictionary];
NSString *actionIdentifier = response.actionIdentifier;
if ([UNNotificationDefaultActionIdentifier isEqualToString:actionIdentifier]) {
actionIdentifier = EXNotificationResponseDefaultActionIdentifier;
}
serializedResponse[@"actionIdentifier"] = actionIdentifier;
serializedResponse[@"notification"] = [self serializedNotification:response.notification];
if ([response isKindOfClass:[UNTextInputNotificationResponse class]]) {
UNTextInputNotificationResponse *textInputResponse = (UNTextInputNotificationResponse *)response;
serializedResponse[@"userText"] = textInputResponse.userText ?: [NSNull null];
}
return serializedResponse;
}
+ (NSDictionary *)serializedNotification:(UNNotification *)notification
{
NSMutableDictionary *serializedNotification = [NSMutableDictionary dictionary];
serializedNotification[@"request"] = [self serializedNotificationRequest:notification.request];
serializedNotification[@"date"] = @(notification.date.timeIntervalSince1970);
return serializedNotification;
}
+ (NSDictionary *)serializedNotificationRequest:(UNNotificationRequest *)request
{
NSMutableDictionary *serializedRequest = [NSMutableDictionary dictionary];
serializedRequest[@"identifier"] = request.identifier;
serializedRequest[@"content"] = [self serializedNotificationContent:request];
serializedRequest[@"trigger"] = [self serializedNotificationTrigger:request];
return serializedRequest;
}
+ (NSDictionary *)serializedNotificationContent:(UNNotificationRequest *)request
{
UNNotificationContent *content = request.content;
NSMutableDictionary *serializedContent = [NSMutableDictionary dictionary];
serializedContent[@"title"] = content.title ?: [NSNull null];
serializedContent[@"subtitle"] = content.subtitle ?: [NSNull null];
serializedContent[@"body"] = content.body ?: [NSNull null];
serializedContent[@"badge"] = content.badge ?: [NSNull null];
serializedContent[@"sound"] = [self serializedNotificationSound:content.sound] ?: [NSNull null];
serializedContent[@"launchImageName"] = content.launchImageName ?: [NSNull null];
serializedContent[@"data"] = [self serializedNotificationData:request] ?: [NSNull null];
serializedContent[@"attachments"] = [self serializedNotificationAttachments:content.attachments];
if (@available(iOS 12.0, *)) {
serializedContent[@"summaryArgument"] = content.summaryArgument ?: [NSNull null];
serializedContent[@"summaryArgumentCount"] = @(content.summaryArgumentCount);
}
serializedContent[@"categoryIdentifier"] = content.categoryIdentifier ? content.categoryIdentifier : [NSNull null];
serializedContent[@"threadIdentifier"] = content.threadIdentifier ?: [NSNull null];
if (@available(iOS 13.0, *)) {
serializedContent[@"targetContentIdentifier"] = content.targetContentIdentifier ?: [NSNull null];
}
if (@available(iOS 15.0, *)) {
serializedContent[@"interruptionLevel"] = [EXNotificationSerializer serializedInterruptionLevel:content.interruptionLevel];
}
return serializedContent;
}
+ (NSString *)serializedInterruptionLevel:(UNNotificationInterruptionLevel)interruptionLevel API_AVAILABLE(ios(15.0)) {
static NSDictionary<NSNumber *, NSString *> *interruptionLevelMap;
if (!interruptionLevelMap) {
interruptionLevelMap = @{
@(UNNotificationInterruptionLevelPassive): @"passive",
@(UNNotificationInterruptionLevelActive): @"active",
@(UNNotificationInterruptionLevelTimeSensitive): @"timeSensitive",
@(UNNotificationInterruptionLevelCritical): @"critical"
};
}
return [interruptionLevelMap objectForKey:@(interruptionLevel)];
}
+ (NSDictionary *)serializedNotificationData:(UNNotificationRequest *)request
{
BOOL isRemote = [request.trigger isKindOfClass:[UNPushNotificationTrigger class]];
return isRemote ? request.content.userInfo[@"body"] : request.content.userInfo;
}
+ (NSString *)serializedNotificationSound:(UNNotificationSound *)sound
{
// nil compared to defaultCriticalSound returns true
if (!sound) {
return nil;
}
if (@available(iOS 12.0, *)) {
if ([[UNNotificationSound defaultCriticalSound] isEqual:sound]) {
return @"defaultCritical";
}
}
if ([[UNNotificationSound defaultSound] isEqual:sound]) {
return @"default";
}
return @"custom";
}
+ (NSArray *)serializedNotificationAttachments:(NSArray<UNNotificationAttachment *> *)attachments
{
NSMutableArray *serializedAttachments = [NSMutableArray array];
for (UNNotificationAttachment *attachment in attachments) {
[serializedAttachments addObject:[self serializedNotificationAttachment:attachment]];
}
return serializedAttachments;
}
+ (NSDictionary *)serializedNotificationAttachment:(UNNotificationAttachment *)attachment
{
NSMutableDictionary *serializedAttachment = [NSMutableDictionary dictionary];
serializedAttachment[@"identifier"] = attachment.identifier ?: [NSNull null];
serializedAttachment[@"url"] = attachment.URL.absoluteString ?: [NSNull null];
serializedAttachment[@"type"] = attachment.type ?: [NSNull null];
return serializedAttachment;
}
+ (NSDictionary *)serializedNotificationTrigger:(UNNotificationRequest *)request
{
UNNotificationTrigger *trigger = request.trigger;
NSMutableDictionary *serializedTrigger = [NSMutableDictionary dictionary];
serializedTrigger[@"class"] = NSStringFromClass(trigger.class);
if ([trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
serializedTrigger[@"type"] = @"push";
serializedTrigger[@"payload"] = request.content.userInfo;
} else if ([trigger isKindOfClass:[UNCalendarNotificationTrigger class]]) {
serializedTrigger[@"type"] = @"calendar";
serializedTrigger[@"repeats"] = @(trigger.repeats);
UNCalendarNotificationTrigger *calendarTrigger = (UNCalendarNotificationTrigger *)trigger;
serializedTrigger[@"dateComponents"] = [self serializedDateComponents:calendarTrigger.dateComponents];
#if !(TARGET_OS_MACCATALYST)
} else if ([trigger isKindOfClass:[UNLocationNotificationTrigger class]]) {
serializedTrigger[@"type"] = @"location";
serializedTrigger[@"repeats"] = @(trigger.repeats);
UNLocationNotificationTrigger *locationTrigger = (UNLocationNotificationTrigger *)trigger;
serializedTrigger[@"region"] = [self serializedRegion:locationTrigger.region];
#endif
} else if ([trigger isKindOfClass:[UNTimeIntervalNotificationTrigger class]]) {
serializedTrigger[@"type"] = @"timeInterval";
UNTimeIntervalNotificationTrigger *timeIntervalTrigger = (UNTimeIntervalNotificationTrigger *)trigger;
serializedTrigger[@"seconds"] = @(timeIntervalTrigger.timeInterval);
serializedTrigger[@"repeats"] = @(trigger.repeats);
} else {
serializedTrigger[@"type"] = @"unknown";
}
return serializedTrigger;
}
+ (NSDictionary *)serializedDateComponents:(NSDateComponents *)dateComponents
{
NSMutableDictionary *serializedComponents = [NSMutableDictionary dictionary];
NSArray<NSNumber *> *autoConvertedUnits = [[self calendarUnitsConversionMap] allKeys];
for (NSNumber *calendarUnitNumber in autoConvertedUnits) {
NSCalendarUnit calendarUnit = [calendarUnitNumber unsignedIntegerValue];
NSInteger unitValue = [dateComponents valueForComponent:calendarUnit];
if (unitValue != NSDateComponentUndefined) {
serializedComponents[[self keyForCalendarUnit:calendarUnit]] = @([dateComponents valueForComponent:calendarUnit]);
}
}
serializedComponents[@"calendar"] = dateComponents.calendar.calendarIdentifier ?: [NSNull null];
serializedComponents[@"timeZone"] = dateComponents.timeZone.description ?: [NSNull null];
serializedComponents[@"isLeapMonth"] = @(dateComponents.isLeapMonth);
return serializedComponents;
}
+ (NSDictionary *)calendarUnitsConversionMap
{
static NSDictionary *keysMap = nil;
if (!keysMap) {
keysMap = @{
@(NSCalendarUnitEra): @"era",
@(NSCalendarUnitYear): @"year",
@(NSCalendarUnitMonth): @"month",
@(NSCalendarUnitDay): @"day",
@(NSCalendarUnitHour): @"hour",
@(NSCalendarUnitMinute): @"minute",
@(NSCalendarUnitSecond): @"second",
@(NSCalendarUnitWeekday): @"weekday",
@(NSCalendarUnitWeekdayOrdinal): @"weekdayOrdinal",
@(NSCalendarUnitQuarter): @"quarter",
@(NSCalendarUnitWeekOfMonth): @"weekOfMonth",
@(NSCalendarUnitWeekOfYear): @"weekOfYear",
@(NSCalendarUnitYearForWeekOfYear): @"yearForWeekOfYear",
@(NSCalendarUnitNanosecond): @"nanosecond"
// NSCalendarUnitCalendar and NSCalendarUnitTimeZone
// should be handled separately
};
}
return keysMap;
}
+ (NSString *)keyForCalendarUnit:(NSCalendarUnit)calendarUnit
{
return [self calendarUnitsConversionMap][@(calendarUnit)];
}
+ (NSDictionary *)serializedRegion:(CLRegion *)region
{
NSMutableDictionary *serializedRegion = [NSMutableDictionary dictionary];
serializedRegion[@"identifier"] = region.identifier;
serializedRegion[@"notifyOnEntry"] = @(region.notifyOnEntry);
serializedRegion[@"notifyOnExit"] = @(region.notifyOnExit);
if ([region isKindOfClass:[CLCircularRegion class]]) {
serializedRegion[@"type"] = @"circular";
CLCircularRegion *circularRegion = (CLCircularRegion *)region;
NSDictionary *serializedCenter = @{
@"latitude": @(circularRegion.center.latitude),
@"longitude": @(circularRegion.center.longitude)
};
serializedRegion[@"center"] = serializedCenter;
serializedRegion[@"radius"] = @(circularRegion.radius);
} else if ([region isKindOfClass:[CLBeaconRegion class]]) {
serializedRegion[@"type"] = @"beacon";
CLBeaconRegion *beaconRegion = (CLBeaconRegion *)region;
serializedRegion[@"notifyEntryStateOnDisplay"] = @(beaconRegion.notifyEntryStateOnDisplay);
serializedRegion[@"major"] = beaconRegion.major ?: [NSNull null];
serializedRegion[@"minor"] = beaconRegion.minor ?: [NSNull null];
if (@available(iOS 13.0, *)) {
serializedRegion[@"uuid"] = beaconRegion.UUID;
NSDictionary *serializedConstraint = @{
@"uuid": beaconRegion.beaconIdentityConstraint.UUID,
@"major": beaconRegion.beaconIdentityConstraint.major ?: [NSNull null],
@"minor": beaconRegion.beaconIdentityConstraint.minor ?: [NSNull null],
};
serializedRegion[@"beaconIdentityConstraint"] = serializedConstraint;
}
} else {
serializedRegion[@"type"] = @"unknown";
}
return serializedRegion;
}
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,27 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <UserNotifications/UserNotifications.h>
NS_ASSUME_NONNULL_BEGIN
@protocol EXNotificationsDelegate <NSObject>
@optional
# pragma mark - UIApplicationDelegate
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;
# pragma mark - UNUserNotificationCenterDelegate
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler;
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler;
- (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsForNotification:(UNNotification *)notification;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,18 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/EXExportedModule.h>
#import <ExpoModulesCore/EXEventEmitter.h>
#import <ExpoModulesCore/EXModuleRegistryConsumer.h>
#import <ExpoModulesCore/EXEventEmitterService.h>
#import <EXNotifications/EXNotificationsDelegate.h>
static NSString * const onDidReceiveNotification = @"onDidReceiveNotification";
static NSString * const onDidReceiveNotificationResponse = @"onDidReceiveNotificationResponse";
static NSString * const onDidClearNotificationResponse = @"onDidClearNotificationResponse";
@interface EXNotificationsEmitter : EXExportedModule <EXEventEmitter, EXModuleRegistryConsumer, EXNotificationsDelegate>
@property (nonatomic, weak, readonly) id<EXEventEmitterService> eventEmitter;
@end

View File

@@ -0,0 +1,120 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <EXNotifications/EXNotificationsEmitter.h>
#import <EXNotifications/EXNotificationSerializer.h>
#import <EXNotifications/EXNotificationCenterDelegate.h>
#import <ExpoModulesCore/EXEventEmitterService.h>
@interface EXNotificationsEmitter ()
@property (nonatomic, weak) id<EXNotificationCenterDelegate> notificationCenterDelegate;
@property (nonatomic, assign) BOOL isBeingObserved;
@property (nonatomic, assign) BOOL isListening;
@property (nonatomic, weak) id<EXEventEmitterService> eventEmitter;
@end
@implementation EXNotificationsEmitter
EX_EXPORT_MODULE(ExpoNotificationsEmitter);
EX_EXPORT_METHOD_AS(getLastNotificationResponseAsync,
getLastNotificationResponseAsyncWithResolver:(EXPromiseResolveBlock)resolve reject:(EXPromiseRejectBlock)reject)
{
UNNotificationResponse* lastResponse = _notificationCenterDelegate.lastNotificationResponse;
resolve(lastResponse ? [self serializedNotificationResponse:lastResponse] : [NSNull null]);
}
EX_EXPORT_METHOD_AS(clearLastNotificationResponseAsync,
clearLastNotificationResponseAsyncWithResolver:(EXPromiseResolveBlock)resolve reject:(EXPromiseRejectBlock)reject)
{
_notificationCenterDelegate.lastNotificationResponse = nil;
resolve([NSNull null]);
}
# pragma mark - EXModuleRegistryConsumer
- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry
{
_eventEmitter = [moduleRegistry getModuleImplementingProtocol:@protocol(EXEventEmitterService)];
_notificationCenterDelegate = [moduleRegistry getSingletonModuleForName:@"NotificationCenterDelegate"];
}
# pragma mark - EXEventEmitter
- (NSArray<NSString *> *)supportedEvents
{
return @[onDidReceiveNotification, onDidReceiveNotificationResponse, onDidClearNotificationResponse];
}
- (void)startObserving
{
[self setIsBeingObserved:YES];
}
- (void)stopObserving
{
[self setIsBeingObserved:NO];
}
- (void)setIsBeingObserved:(BOOL)isBeingObserved
{
_isBeingObserved = isBeingObserved;
BOOL shouldListen = _isBeingObserved;
if (shouldListen && !_isListening) {
[_notificationCenterDelegate addDelegate:self];
_isListening = YES;
} else if (!shouldListen && _isListening) {
[_notificationCenterDelegate removeDelegate:self];
_isListening = NO;
}
}
# pragma mark - EXNotificationsDelegate
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
// Background task execution would happen here.
completionHandler(UIBackgroundFetchResultNoData);
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
{
_notificationCenterDelegate.lastNotificationResponse = response;
[self sendEventWithName:onDidReceiveNotificationResponse body:[self serializedNotificationResponse:response]];
completionHandler();
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{
[self sendEventWithName:onDidReceiveNotification body:[self serializedNotification:notification]];
completionHandler(UNNotificationPresentationOptionNone);
}
- (void)sendEventWithName:(NSString *)eventName body:(id)body
{
// Silence React Native warning: "Sending ... with no listeners registered."
// See: https://github.com/expo/expo/pull/10883#pullrequestreview-529183413
// While in practice we don't need to verify this, as as of the end of 2020
// we wouldn't send any event to JS if we weren't being observed because
// we wouldn't be subscribed to the notification center delegate it's nice
// to be sure this problem won't ever arise.
if (_isBeingObserved) {
[_eventEmitter sendEventWithName:eventName body:body];
}
}
- (NSDictionary *)serializedNotification:(UNNotification *)notification
{
return [EXNotificationSerializer serializedNotification:notification];
}
- (NSDictionary *)serializedNotificationResponse:(UNNotificationResponse *)notificationResponse
{
return [EXNotificationSerializer serializedNotificationResponse:notificationResponse];
}
@end

View File

@@ -0,0 +1,12 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/EXExportedModule.h>
#import <ExpoModulesCore/EXEventEmitter.h>
#import <ExpoModulesCore/EXModuleRegistryConsumer.h>
#import <EXNotifications/EXNotificationsDelegate.h>
#import <EXNotifications/EXSingleNotificationHandlerTask.h>
@interface EXNotificationsHandlerModule : EXExportedModule <EXEventEmitter, EXModuleRegistryConsumer, EXNotificationsDelegate, EXSingleNotificationHandlerTaskDelegate>
@end

View File

@@ -0,0 +1,109 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <EXNotifications/EXNotificationsHandlerModule.h>
#import <EXNotifications/EXNotificationSerializer.h>
#import <EXNotifications/EXNotificationCenterDelegate.h>
#import <ExpoModulesCore/EXEventEmitterService.h>
@interface EXNotificationsHandlerModule ()
@property (nonatomic, weak) id<EXNotificationCenterDelegate> notificationCenterDelegate;
@property (nonatomic, assign) BOOL isListening;
@property (nonatomic, assign) BOOL isBeingObserved;
@property (nonatomic, weak) id<EXEventEmitterService> eventEmitter;
@property (nonatomic, strong) NSMutableDictionary<NSString *, EXSingleNotificationHandlerTask *> *tasksMap;
@end
@implementation EXNotificationsHandlerModule
EX_EXPORT_MODULE(ExpoNotificationsHandlerModule);
- (instancetype)init
{
if (self = [super init]) {
_tasksMap = [NSMutableDictionary dictionary];
}
return self;
}
# pragma mark - Exported methods
EX_EXPORT_METHOD_AS(handleNotificationAsync,
handleNotificationAsync:(NSString *)identifier withBehavior:(NSDictionary *)behavior resolver:(EXPromiseResolveBlock)resolve rejecter:(EXPromiseRejectBlock)reject)
{
EXSingleNotificationHandlerTask *task = _tasksMap[identifier];
if (!task) {
NSString *message = [NSString stringWithFormat:@"Failed to handle notification %@, it has already been handled.", identifier];
return reject(@"ERR_NOTIFICATION_HANDLED", message, nil);
}
NSError *error = [task handleResponse:behavior];
if (error) {
return reject(error.userInfo[@"code"], error.userInfo[@"message"], error);
} else {
resolve(nil);
}
}
# pragma mark - EXModuleRegistryConsumer
- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry
{
_eventEmitter = [moduleRegistry getModuleImplementingProtocol:@protocol(EXEventEmitterService)];
_notificationCenterDelegate = [moduleRegistry getSingletonModuleForName:@"NotificationCenterDelegate"];
}
# pragma mark - EXEventEmitter
- (NSArray<NSString *> *)supportedEvents
{
return [EXSingleNotificationHandlerTask eventNames];
}
- (void)startObserving
{
[self setIsBeingObserved:YES];
}
- (void)stopObserving
{
[self setIsBeingObserved:NO];
}
- (void)setIsBeingObserved:(BOOL)isBeingObserved
{
_isBeingObserved = isBeingObserved;
BOOL shouldListen = _isBeingObserved;
if (shouldListen && !_isListening) {
[_notificationCenterDelegate addDelegate:self];
_isListening = YES;
} else if (!shouldListen && _isListening) {
[_notificationCenterDelegate removeDelegate:self];
_isListening = NO;
}
}
# pragma mark - EXNotificationsDelegate
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{
EXSingleNotificationHandlerTask *task = [[EXSingleNotificationHandlerTask alloc] initWithEventEmitter:_eventEmitter
notification:notification
completionHandler:completionHandler
delegate:self];
[_tasksMap setObject:task forKey:task.identifier];
[task start];
}
# pragma mark - EXSingleNotificationHandlerTaskDelegate
- (void)taskDidFinish:(EXSingleNotificationHandlerTask *)task
{
[_tasksMap removeObjectForKey:task.identifier];
}
@end

View File

@@ -0,0 +1,33 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
#import <ExpoModulesCore/EXEventEmitterService.h>
#import <UserNotifications/UserNotifications.h>
NS_ASSUME_NONNULL_BEGIN
@class EXSingleNotificationHandlerTask;
@protocol EXSingleNotificationHandlerTaskDelegate
- (void)taskDidFinish:(EXSingleNotificationHandlerTask *)task;
@end
@interface EXSingleNotificationHandlerTask : NSObject
+ (NSArray<NSString *> *)eventNames;
- (instancetype)initWithEventEmitter:(id<EXEventEmitterService>)eventEmitter
notification:(UNNotification *)notification
completionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
delegate:(id<EXSingleNotificationHandlerTaskDelegate>)delegate;
- (NSString *)identifier;
- (void)start;
- (nullable NSError *)handleResponse:(NSDictionary *)response;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,124 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <EXNotifications/EXSingleNotificationHandlerTask.h>
#import <EXNotifications/EXNotificationSerializer.h>
static NSString * const onHandleNotification = @"onHandleNotification";
static NSString * const onHandleNotificationTimeout = @"onHandleNotificationTimeout";
static NSString * const shouldShowAlertKey = @"shouldShowAlert";
static NSString * const shouldPlaySoundKey = @"shouldPlaySound";
static NSString * const shouldSetBadgeKey = @"shouldSetBadge";
static NSTimeInterval const secondsToTimeout = 3;
static NSString * const EXNotificationHandlerErrorDomain = @"expo.notifications.handler";
@interface EXSingleNotificationHandlerTask ()
@property (nonatomic, weak) id<EXEventEmitterService> eventEmitter;
@property (nonatomic, strong) UNNotification *notification;
@property (nonatomic, copy) void (^completionHandler)(UNNotificationPresentationOptions);
@property (nonatomic, weak) id<EXSingleNotificationHandlerTaskDelegate> delegate;
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation EXSingleNotificationHandlerTask
+ (NSArray<NSString *> *)eventNames
{
return @[onHandleNotification, onHandleNotificationTimeout];
}
- (instancetype)initWithEventEmitter:(id<EXEventEmitterService>)eventEmitter
notification:(UNNotification *)notification
completionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
delegate:(nonnull id<EXSingleNotificationHandlerTaskDelegate>)delegate
{
if (self = [super init]) {
_eventEmitter = eventEmitter;
_notification = notification;
_completionHandler = completionHandler;
_delegate = delegate;
}
return self;
}
- (NSString *)identifier
{
return _notification.request.identifier;
}
- (void)start
{
[_eventEmitter sendEventWithName:onHandleNotification body:@{
@"id": _notification.request.identifier,
@"notification": [EXNotificationSerializer serializedNotification:_notification]
}];
_timer = [NSTimer scheduledTimerWithTimeInterval:secondsToTimeout target:self selector:@selector(handleTimeout) userInfo:nil repeats:NO];
}
- (nullable NSError *)handleResponse:(NSDictionary *)response
{
@synchronized (self) {
NSError *maybeError = [self callCompletionHandlerWithOptions:[self presentationOptionsFromResponse:response]];
[self finish];
return maybeError;
}
}
- (void)handleTimeout
{
@synchronized (self) {
[_eventEmitter sendEventWithName:onHandleNotificationTimeout body:@{
@"id": _notification.request.identifier,
@"notification": [EXNotificationSerializer serializedNotification:_notification]
}];
[self callCompletionHandlerWithOptions:UNNotificationPresentationOptionNone];
[self finish];
}
}
- (nullable NSError *)callCompletionHandlerWithOptions:(UNNotificationPresentationOptions)options
{
if (_completionHandler) {
_completionHandler(options);
_completionHandler = nil;
return nil;
} else {
return [NSError errorWithDomain:EXNotificationHandlerErrorDomain code:-1 userInfo:@{
@"code": @"ERR_NOTIFICATION_RESPONSE_TIMEOUT",
@"message": @"Notification has already been handled. Most probably the request has timed out."
}];
}
}
- (void)finish
{
[_timer invalidate];
_timer = nil;
[_delegate taskDidFinish:self];
}
- (UNNotificationPresentationOptions)presentationOptionsFromResponse:(NSDictionary *)response
{
UNNotificationPresentationOptions options = UNNotificationPresentationOptionNone;
// TODO(iOS 14): use UNNotificationPresentationOptionList and UNNotificationPresentationOptionBanner
if ([response[shouldShowAlertKey] boolValue]) {
options |= UNNotificationPresentationOptionAlert;
}
if ([response[shouldPlaySoundKey] boolValue]) {
options |= UNNotificationPresentationOptionSound;
}
if ([response[shouldSetBadgeKey] boolValue]) {
options |= UNNotificationPresentationOptionBadge;
}
return options;
}
@end

View File

@@ -0,0 +1,7 @@
#import <Foundation/Foundation.h>
@interface NSDictionary (EXNotificationsVerifyingClass)
- (id)objectForKey:(id)aKey verifyingClass:(__unsafe_unretained Class)klass;
@end

View File

@@ -0,0 +1,19 @@
#import <EXNotifications/NSDictionary+EXNotificationsVerifyingClass.h>
static NSString * const invalidValueExceptionName = @"Value of invalid class encountered";
static NSString * const invalidValueClassReasonFormat = @"Value under key `%@` is of class %@, while %@ was expected.";
@implementation NSDictionary (EXNotificationsVerifyingClass)
- (id)objectForKey:(id)aKey verifyingClass:(__unsafe_unretained Class)klass
{
id obj = [self objectForKey:aKey];
if (!obj || [obj isKindOfClass:klass]) {
return obj;
}
NSString *reason = [NSString stringWithFormat:invalidValueClassReasonFormat, aKey, NSStringFromClass([obj class]), NSStringFromClass(klass)];
@throw [NSException exceptionWithName:invalidValueExceptionName reason:reason userInfo:nil];
}
@end

View File

@@ -0,0 +1,13 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/EXExportedModule.h>
#import <ExpoModulesCore/EXModuleRegistryConsumer.h>
#import <EXNotifications/EXNotificationsDelegate.h>
@interface EXNotificationPresentationModule : EXExportedModule <EXModuleRegistryConsumer, EXNotificationsDelegate>
- (NSArray * _Nonnull)serializeNotifications:(NSArray<UNNotification *> * _Nonnull)notifications;
- (void)dismissNotificationWithIdentifier:(NSString *)identifier resolve:(EXPromiseResolveBlock)resolve reject:(EXPromiseRejectBlock)reject;
@end

View File

@@ -0,0 +1,124 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <EXNotifications/EXNotificationPresentationModule.h>
#import <EXNotifications/EXNotificationBuilder.h>
#import <EXNotifications/EXNotificationSerializer.h>
#import <EXNotifications/EXNotificationCenterDelegate.h>
@interface EXNotificationPresentationModule ()
@property (nonatomic, weak) id<EXNotificationBuilder> notificationBuilder;
// Remove once presentNotificationAsync is removed
@property (nonatomic, strong) NSCountedSet<NSString *> *presentedNotifications;
@property (nonatomic, weak) id<EXNotificationCenterDelegate> notificationCenterDelegate;
@end
@implementation EXNotificationPresentationModule
EX_EXPORT_MODULE(ExpoNotificationPresenter);
// Remove once presentNotificationAsync is removed
- (instancetype)init
{
if (self = [super init]) {
_presentedNotifications = [NSCountedSet set];
}
return self;
}
# pragma mark - Exported methods
// Remove once presentNotificationAsync is removed
EX_EXPORT_METHOD_AS(presentNotificationAsync,
presentNotificationWithIdentifier:(NSString *)identifier
notification:(NSDictionary *)notificationSpec
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
UNNotificationContent *content = [_notificationBuilder notificationContentFromRequest:notificationSpec];
UNNotificationTrigger *trigger = nil;
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
[_presentedNotifications addObject:identifier];
__weak EXNotificationPresentationModule *weakSelf = self;
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (error) {
// If there was no error, willPresentNotification: callback will remove the identifier from the set
[weakSelf.presentedNotifications removeObject:identifier];
NSString *message = [NSString stringWithFormat:@"Notification could not have been presented: %@", error.description];
reject(@"ERR_NOTIF_PRESENT", message, error);
} else {
resolve(identifier);
}
}];
}
EX_EXPORT_METHOD_AS(getPresentedNotificationsAsync,
getPresentedNotificationsAsyncWithResolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
[[UNUserNotificationCenter currentNotificationCenter] getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> * _Nonnull notifications) {
resolve([self serializeNotifications:notifications]);
}];
}
EX_EXPORT_METHOD_AS(dismissNotificationAsync,
dismissNotificationWithIdentifier:(NSString *)identifier
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
[[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:@[identifier]];
resolve(nil);
}
EX_EXPORT_METHOD_AS(dismissAllNotificationsAsync,
dismissAllNotificationsWithResolver:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
[[UNUserNotificationCenter currentNotificationCenter] removeAllDeliveredNotifications];
resolve(nil);
}
# pragma mark - EXModuleRegistryConsumer
- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry
{
_notificationBuilder = [moduleRegistry getModuleImplementingProtocol:@protocol(EXNotificationBuilder)];
// Remove once presentNotificationAsync is removed
id<EXNotificationCenterDelegate> notificationCenterDelegate = (id<EXNotificationCenterDelegate>)[moduleRegistry getSingletonModuleForName:@"NotificationCenterDelegate"];
[notificationCenterDelegate addDelegate:self];
}
// Remove once presentNotificationAsync is removed
# pragma mark - EXNotificationsDelegate
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{
UNNotificationPresentationOptions presentationOptions = UNNotificationPresentationOptionNone;
NSString *identifier = notification.request.identifier;
if ([_presentedNotifications containsObject:identifier]) {
[_presentedNotifications removeObject:identifier];
// TODO(iOS 14): use UNNotificationPresentationOptionList and UNNotificationPresentationOptionBanner
presentationOptions = UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge;
}
completionHandler(presentationOptions);
}
# pragma mark - Helpers
- (NSArray * _Nonnull)serializeNotifications:(NSArray<UNNotification *> * _Nonnull)notifications
{
NSMutableArray *serializedNotifications = [NSMutableArray new];
for (UNNotification *notification in notifications) {
[serializedNotifications addObject:[EXNotificationSerializer serializedNotification:notification]];
}
return serializedNotifications;
}
@end

View File

@@ -0,0 +1,21 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/EXExportedModule.h>
#import <ExpoModulesCore/EXModuleRegistryConsumer.h>
#import <UserNotifications/UserNotifications.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXNotificationSchedulerModule : EXExportedModule <EXModuleRegistryConsumer>
- (NSArray * _Nonnull)serializeNotificationRequests:(NSArray<UNNotificationRequest *> * _Nonnull) requests;
- (void)cancelNotification:(NSString *)identifier resolve:(EXPromiseResolveBlock)resolve rejecting:(EXPromiseRejectBlock)reject;
- (void)cancelAllNotificationsWithResolver:(EXPromiseResolveBlock)resolve rejecting:(EXPromiseRejectBlock)reject;
- (UNNotificationRequest *)buildNotificationRequestWithIdentifier:(NSString *)identifier content:(NSDictionary *)contentInput trigger:(NSDictionary *)triggerInput;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,263 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <EXNotifications/EXNotificationSchedulerModule.h>
#import <EXNotifications/EXNotificationSerializer.h>
#import <EXNotifications/EXNotificationBuilder.h>
#import <EXNotifications/NSDictionary+EXNotificationsVerifyingClass.h>
#import <UserNotifications/UserNotifications.h>
static NSString * const notificationTriggerTypeKey = @"type";
static NSString * const notificationTriggerRepeatsKey = @"repeats";
static NSString * const intervalNotificationTriggerType = @"timeInterval";
static NSString * const intervalNotificationTriggerIntervalKey = @"seconds";
static NSString * const dailyNotificationTriggerType = @"daily";
static NSString * const dailyNotificationTriggerHourKey = @"hour";
static NSString * const dailyNotificationTriggerMinuteKey = @"minute";
static NSString * const weeklyNotificationTriggerType = @"weekly";
static NSString * const weeklyNotificationTriggerWeekdayKey = @"weekday";
static NSString * const weeklyNotificationTriggerHourKey = @"hour";
static NSString * const weeklyNotificationTriggerMinuteKey = @"minute";
static NSString * const yearlyNotificationTriggerType = @"yearly";
static NSString * const yearlyNotificationTriggerDayKey = @"day";
static NSString * const yearlyNotificationTriggerMonthKey = @"month";
static NSString * const yearlyNotificationTriggerHourKey = @"hour";
static NSString * const yearlyNotificationTriggerMinuteKey = @"minute";
static NSString * const dateNotificationTriggerType = @"date";
static NSString * const dateNotificationTriggerTimestampKey = @"timestamp";
static NSString * const calendarNotificationTriggerType = @"calendar";
static NSString * const calendarNotificationTriggerComponentsKey = @"value";
static NSString * const calendarNotificationTriggerTimezoneKey = @"timezone";
@interface EXNotificationSchedulerModule ()
@property (nonatomic, weak) id<EXNotificationBuilder> builder;
@end
@implementation EXNotificationSchedulerModule
EX_EXPORT_MODULE(ExpoNotificationScheduler);
- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry
{
_builder = [moduleRegistry getModuleImplementingProtocol:@protocol(EXNotificationBuilder)];
}
# pragma mark - Exported methods
EX_EXPORT_METHOD_AS(getAllScheduledNotificationsAsync,
getAllScheduledNotifications:(EXPromiseResolveBlock)resolve reject:(EXPromiseRejectBlock)reject
)
{
[[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:^(NSArray<UNNotificationRequest *> * _Nonnull requests) {
resolve([self serializeNotificationRequests:requests]);
}];
}
EX_EXPORT_METHOD_AS(scheduleNotificationAsync,
scheduleNotification:(NSString *)identifier notificationSpec:(NSDictionary *)notificationSpec triggerSpec:(NSDictionary *)triggerSpec resolve:(EXPromiseResolveBlock)resolve rejecting:(EXPromiseRejectBlock)reject)
{
@try {
UNNotificationRequest *request = [self buildNotificationRequestWithIdentifier:identifier content:notificationSpec trigger:triggerSpec];
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (error) {
NSString *message = [NSString stringWithFormat:@"Failed to schedule notification. %@", error];
reject(@"ERR_NOTIFICATIONS_FAILED_TO_SCHEDULE", message, error);
} else {
resolve(identifier);
}
}];
} @catch (NSException *exception) {
NSString *message = [NSString stringWithFormat:@"Failed to schedule notification. %@", exception];
reject(@"ERR_NOTIFICATIONS_FAILED_TO_SCHEDULE", message, nil);
}
}
EX_EXPORT_METHOD_AS(cancelScheduledNotificationAsync,
cancelNotification:(NSString *)identifier resolve:(EXPromiseResolveBlock)resolve rejecting:(EXPromiseRejectBlock)reject)
{
[[UNUserNotificationCenter currentNotificationCenter] removePendingNotificationRequestsWithIdentifiers:@[identifier]];
resolve(nil);
}
EX_EXPORT_METHOD_AS(cancelAllScheduledNotificationsAsync,
cancelAllNotificationsWithResolver:(EXPromiseResolveBlock)resolve rejecting:(EXPromiseRejectBlock)reject)
{
[[UNUserNotificationCenter currentNotificationCenter] removeAllPendingNotificationRequests];
resolve(nil);
}
EX_EXPORT_METHOD_AS(getNextTriggerDateAsync,
getNextTriggerDate:(NSDictionary *)triggerSpec resolve:(EXPromiseResolveBlock)resolve rejecting:(EXPromiseRejectBlock)reject)
{
@try {
UNNotificationTrigger *trigger = [self triggerFromParams:triggerSpec];
if ([trigger isKindOfClass:[UNCalendarNotificationTrigger class]]) {
UNCalendarNotificationTrigger *calendarTrigger = (UNCalendarNotificationTrigger *)trigger;
NSDate *nextTriggerDate = [calendarTrigger nextTriggerDate];
// We want to return milliseconds from this method.
resolve(nextTriggerDate ? @([nextTriggerDate timeIntervalSince1970] * 1000) : [NSNull null]);
} else if ([trigger isKindOfClass:[UNTimeIntervalNotificationTrigger class]]) {
UNTimeIntervalNotificationTrigger *timeIntervalTrigger = (UNTimeIntervalNotificationTrigger *)trigger;
NSDate *nextTriggerDate = [timeIntervalTrigger nextTriggerDate];
// We want to return milliseconds from this method.
resolve(nextTriggerDate ? @([nextTriggerDate timeIntervalSince1970] * 1000) : [NSNull null]);
} else {
NSString *message = [NSString stringWithFormat:@"It is not possible to get next trigger date for triggers other than calendar-based. Provided trigger resulted in %@ trigger.", NSStringFromClass([trigger class])];
reject(@"ERR_NOTIFICATIONS_INVALID_CALENDAR_TRIGGER", message, nil);
}
} @catch (NSException *exception) {
NSString *message = [NSString stringWithFormat:@"Failed to get next trigger date. %@", exception];
reject(@"ERR_NOTIFICATIONS_FAILED_TO_GET_NEXT_TRIGGER_DATE", message, nil);
}
}
- (UNNotificationRequest *)buildNotificationRequestWithIdentifier:(NSString *)identifier
content:(NSDictionary *)contentInput
trigger:(NSDictionary *)triggerInput
{
UNNotificationContent *content = [_builder notificationContentFromRequest:contentInput];
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:[self triggerFromParams:triggerInput]];
return request;
}
- (NSArray * _Nonnull)serializeNotificationRequests:(NSArray<UNNotificationRequest *> * _Nonnull) requests
{
NSMutableArray *serializedRequests = [NSMutableArray new];
for (UNNotificationRequest *request in requests) {
[serializedRequests addObject:[EXNotificationSerializer serializedNotificationRequest:request]];
}
return serializedRequests;
}
- (UNNotificationTrigger *)triggerFromParams:(NSDictionary *)params
{
if (!params) {
// nil trigger is a valid trigger
return nil;
}
if (![params isKindOfClass:[NSDictionary class]]) {
NSString *reason = [NSString stringWithFormat:@"Unknown notification trigger declaration passed in, expected a dictionary, received %@.", NSStringFromClass(params.class)];
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil];
}
NSString *triggerType = params[notificationTriggerTypeKey];
if ([intervalNotificationTriggerType isEqualToString:triggerType]) {
NSNumber *interval = [params objectForKey:intervalNotificationTriggerIntervalKey verifyingClass:[NSNumber class]];
NSNumber *repeats = [params objectForKey:notificationTriggerRepeatsKey verifyingClass:[NSNumber class]];
return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:[interval unsignedIntegerValue]
repeats:[repeats boolValue]];
} else if ([dateNotificationTriggerType isEqualToString:triggerType]) {
NSNumber *timestampMs = [params objectForKey:dateNotificationTriggerTimestampKey verifyingClass:[NSNumber class]];
NSUInteger timestamp = [timestampMs unsignedIntegerValue] / 1000;
NSDate *date = [NSDate dateWithTimeIntervalSince1970:timestamp];
return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:[date timeIntervalSinceNow]
repeats:NO];
} else if ([dailyNotificationTriggerType isEqualToString:triggerType]) {
NSNumber *hour = [params objectForKey:dailyNotificationTriggerHourKey verifyingClass:[NSNumber class]];
NSNumber *minute = [params objectForKey:dailyNotificationTriggerMinuteKey verifyingClass:[NSNumber class]];
NSDateComponents *dateComponents = [NSDateComponents new];
dateComponents.hour = [hour integerValue];
dateComponents.minute = [minute integerValue];
return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents
repeats:YES];
} else if ([weeklyNotificationTriggerType isEqualToString:triggerType]) {
NSNumber *weekday = [params objectForKey:weeklyNotificationTriggerWeekdayKey verifyingClass:[NSNumber class]];
NSNumber *hour = [params objectForKey:weeklyNotificationTriggerHourKey verifyingClass:[NSNumber class]];
NSNumber *minute = [params objectForKey:weeklyNotificationTriggerMinuteKey verifyingClass:[NSNumber class]];
NSDateComponents *dateComponents = [NSDateComponents new];
dateComponents.weekday = [weekday integerValue];
dateComponents.hour = [hour integerValue];
dateComponents.minute = [minute integerValue];
return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents
repeats:YES];
} else if ([yearlyNotificationTriggerType isEqualToString:triggerType]) {
NSNumber *day = [params objectForKey:yearlyNotificationTriggerDayKey verifyingClass:[NSNumber class]];
NSNumber *month = [params objectForKey:yearlyNotificationTriggerMonthKey verifyingClass:[NSNumber class]];
NSNumber *hour = [params objectForKey:yearlyNotificationTriggerHourKey verifyingClass:[NSNumber class]];
NSNumber *minute = [params objectForKey:yearlyNotificationTriggerMinuteKey verifyingClass:[NSNumber class]];
NSDateComponents *dateComponents = [NSDateComponents new];
dateComponents.day = [day integerValue];
dateComponents.month = [month integerValue] + 1; // iOS uses 1-12 based numbers for months
dateComponents.hour = [hour integerValue];
dateComponents.minute = [minute integerValue];
return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents
repeats:YES];
} else if ([calendarNotificationTriggerType isEqualToString:triggerType]) {
NSDateComponents *dateComponents = [self dateComponentsFromParams:params[calendarNotificationTriggerComponentsKey]];
NSNumber *repeats = [params objectForKey:notificationTriggerRepeatsKey verifyingClass:[NSNumber class]];
return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents
repeats:[repeats boolValue]];
} else {
NSString *reason = [NSString stringWithFormat:@"Unknown notification trigger type: %@.", triggerType];
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil];
}
}
- (NSDateComponents *)dateComponentsFromParams:(NSDictionary<NSString *, id> *)params
{
NSDateComponents *dateComponents = [NSDateComponents new];
// TODO: Verify that DoW matches JS getDay()
dateComponents.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601];
if ([params objectForKey:calendarNotificationTriggerTimezoneKey verifyingClass:[NSString class]]) {
dateComponents.timeZone = [[NSTimeZone alloc] initWithName:params[calendarNotificationTriggerTimezoneKey]];
}
for (NSString *key in [self automatchedDateComponentsKeys]) {
if (params[key]) {
NSNumber *value = [params objectForKey:key verifyingClass:[NSNumber class]];
[dateComponents setValue:[value unsignedIntegerValue] forComponent:[self calendarUnitFor:key]];
}
}
return dateComponents;
}
- (NSDictionary<NSString *, NSNumber *> *)dateComponentsMatchMap
{
static NSDictionary *map;
if (!map) {
map = @{
@"year": @(NSCalendarUnitYear),
@"month": @(NSCalendarUnitMonth),
@"day": @(NSCalendarUnitDay),
@"hour": @(NSCalendarUnitHour),
@"minute": @(NSCalendarUnitMinute),
@"second": @(NSCalendarUnitSecond),
@"weekday": @(NSCalendarUnitWeekday),
@"weekOfMonth": @(NSCalendarUnitWeekOfMonth),
@"weekOfYear": @(NSCalendarUnitWeekOfYear),
@"weekdayOrdinal": @(NSCalendarUnitWeekdayOrdinal)
};
}
return map;
}
- (NSArray<NSString *> *)automatchedDateComponentsKeys
{
return [[self dateComponentsMatchMap] allKeys];
}
- (NSCalendarUnit)calendarUnitFor:(NSString *)key
{
return [[self dateComponentsMatchMap][key] unsignedIntegerValue];
}
@end