1// Copyright 2018-present 650 Industries. All rights reserved. 2 3#import <EXNotifications/EXNotificationSchedulerModule.h> 4#import <EXNotifications/EXNotificationSerializer.h> 5#import <EXNotifications/EXNotificationBuilder.h> 6#import <EXNotifications/NSDictionary+EXNotificationsVerifyingClass.h> 7 8#import <UserNotifications/UserNotifications.h> 9 10static NSString * const notificationTriggerTypeKey = @"type"; 11static NSString * const notificationTriggerRepeatsKey = @"repeats"; 12 13static NSString * const intervalNotificationTriggerType = @"timeInterval"; 14static NSString * const intervalNotificationTriggerIntervalKey = @"seconds"; 15 16static NSString * const dailyNotificationTriggerType = @"daily"; 17static NSString * const dailyNotificationTriggerHourKey = @"hour"; 18static NSString * const dailyNotificationTriggerMinuteKey = @"minute"; 19 20static NSString * const dateNotificationTriggerType = @"date"; 21static NSString * const dateNotificationTriggerTimestampKey = @"timestamp"; 22 23static NSString * const calendarNotificationTriggerType = @"calendar"; 24static NSString * const calendarNotificationTriggerComponentsKey = @"value"; 25static NSString * const calendarNotificationTriggerTimezoneKey = @"timezone"; 26 27 28 29@interface EXNotificationSchedulerModule () 30 31@property (nonatomic, weak) id<EXNotificationBuilder> builder; 32 33@end 34 35@implementation EXNotificationSchedulerModule 36 37UM_EXPORT_MODULE(ExpoNotificationScheduler); 38 39- (void)setModuleRegistry:(UMModuleRegistry *)moduleRegistry 40{ 41 _builder = [moduleRegistry getModuleImplementingProtocol:@protocol(EXNotificationBuilder)]; 42} 43 44# pragma mark - Exported methods 45 46UM_EXPORT_METHOD_AS(getAllScheduledNotificationsAsync, 47 getAllScheduledNotifications:(UMPromiseResolveBlock)resolve reject:(UMPromiseRejectBlock)reject 48 ) 49{ 50 [[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:^(NSArray<UNNotificationRequest *> * _Nonnull requests) { 51 resolve([self serializeNotificationRequests:requests]); 52 }]; 53} 54 55UM_EXPORT_METHOD_AS(scheduleNotificationAsync, 56 scheduleNotification:(NSString *)identifier notificationSpec:(NSDictionary *)notificationSpec triggerSpec:(NSDictionary *)triggerSpec resolve:(UMPromiseResolveBlock)resolve rejecting:(UMPromiseRejectBlock)reject) 57{ 58 @try { 59 UNNotificationContent *content = [_builder notificationContentFromRequest:notificationSpec]; 60 UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:[self triggerFromParams:triggerSpec]]; 61 [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { 62 if (error) { 63 NSString *message = [NSString stringWithFormat:@"Failed to schedule notification. %@", error]; 64 reject(@"ERR_NOTIFICATIONS_FAILED_TO_SCHEDULE", message, error); 65 } else { 66 resolve(identifier); 67 } 68 }]; 69 } @catch (NSException *exception) { 70 NSString *message = [NSString stringWithFormat:@"Failed to schedule notification. %@", exception]; 71 reject(@"ERR_NOTIFICATIONS_FAILED_TO_SCHEDULE", message, nil); 72 } 73} 74 75UM_EXPORT_METHOD_AS(cancelScheduledNotificationAsync, 76 cancelNotification:(NSString *)identifier resolve:(UMPromiseResolveBlock)resolve rejecting:(UMPromiseRejectBlock)reject) 77{ 78 [[UNUserNotificationCenter currentNotificationCenter] removePendingNotificationRequestsWithIdentifiers:@[identifier]]; 79 resolve(nil); 80} 81 82UM_EXPORT_METHOD_AS(cancelAllScheduledNotificationsAsync, 83 cancelAllNotificationsWithResolver:(UMPromiseResolveBlock)resolve rejecting:(UMPromiseRejectBlock)reject) 84{ 85 [[UNUserNotificationCenter currentNotificationCenter] removeAllPendingNotificationRequests]; 86 resolve(nil); 87} 88 89- (NSArray * _Nonnull)serializeNotificationRequests:(NSArray<UNNotificationRequest *> * _Nonnull) requests 90{ 91 NSMutableArray *serializedRequests = [NSMutableArray new]; 92 for (UNNotificationRequest *request in requests) { 93 [serializedRequests addObject:[EXNotificationSerializer serializedNotificationRequest:request]]; 94 } 95 return serializedRequests; 96} 97 98- (UNNotificationTrigger *)triggerFromParams:(NSDictionary *)params 99{ 100 if (!params) { 101 // nil trigger is a valid trigger 102 return nil; 103 } 104 if (![params isKindOfClass:[NSDictionary class]]) { 105 NSString *reason = [NSString stringWithFormat:@"Unknown notification trigger declaration passed in, expected a dictionary, received %@.", NSStringFromClass(params.class)]; 106 @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; 107 } 108 NSString *triggerType = params[notificationTriggerTypeKey]; 109 if ([intervalNotificationTriggerType isEqualToString:triggerType]) { 110 NSNumber *interval = [params objectForKey:intervalNotificationTriggerIntervalKey verifyingClass:[NSNumber class]]; 111 NSNumber *repeats = [params objectForKey:notificationTriggerRepeatsKey verifyingClass:[NSNumber class]]; 112 113 return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:[interval unsignedIntegerValue] 114 repeats:[repeats boolValue]]; 115 } else if ([dateNotificationTriggerType isEqualToString:triggerType]) { 116 NSNumber *timestampMs = [params objectForKey:dateNotificationTriggerTimestampKey verifyingClass:[NSNumber class]]; 117 NSUInteger timestamp = [timestampMs unsignedIntegerValue] / 1000; 118 NSDate *date = [NSDate dateWithTimeIntervalSince1970:timestamp]; 119 120 return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:[date timeIntervalSinceNow] 121 repeats:NO]; 122 123 } else if ([dailyNotificationTriggerType isEqualToString:triggerType]) { 124 NSNumber *hour = [params objectForKey:dailyNotificationTriggerHourKey verifyingClass:[NSNumber class]]; 125 NSNumber *minute = [params objectForKey:dailyNotificationTriggerMinuteKey verifyingClass:[NSNumber class]]; 126 NSDateComponents *dateComponents = [NSDateComponents new]; 127 dateComponents.hour = [hour integerValue]; 128 dateComponents.minute = [minute integerValue]; 129 130 return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents 131 repeats:YES]; 132 } else if ([calendarNotificationTriggerType isEqualToString:triggerType]) { 133 NSDateComponents *dateComponents = [self dateComponentsFromParams:params[calendarNotificationTriggerComponentsKey]]; 134 NSNumber *repeats = [params objectForKey:notificationTriggerRepeatsKey verifyingClass:[NSNumber class]]; 135 136 return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents 137 repeats:[repeats boolValue]]; 138 } else { 139 NSString *reason = [NSString stringWithFormat:@"Unknown notification trigger type: %@.", triggerType]; 140 @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; 141 } 142} 143 144- (NSDateComponents *)dateComponentsFromParams:(NSDictionary<NSString *, id> *)params 145{ 146 NSDateComponents *dateComponents = [NSDateComponents new]; 147 148 // TODO: Verify that DoW matches JS getDay() 149 dateComponents.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601]; 150 151 if ([params objectForKey:calendarNotificationTriggerTimezoneKey verifyingClass:[NSString class]]) { 152 dateComponents.timeZone = [[NSTimeZone alloc] initWithName:params[calendarNotificationTriggerTimezoneKey]]; 153 } 154 155 for (NSString *key in [self automatchedDateComponentsKeys]) { 156 if (params[key]) { 157 NSNumber *value = [params objectForKey:key verifyingClass:[NSNumber class]]; 158 [dateComponents setValue:[value unsignedIntegerValue] forComponent:[self calendarUnitFor:key]]; 159 } 160 } 161 162 return dateComponents; 163} 164 165- (NSDictionary<NSString *, NSNumber *> *)dateComponentsMatchMap 166{ 167 static NSDictionary *map; 168 if (!map) { 169 map = @{ 170 @"year": @(NSCalendarUnitYear), 171 @"month": @(NSCalendarUnitMonth), 172 @"day": @(NSCalendarUnitDay), 173 @"hour": @(NSCalendarUnitHour), 174 @"minute": @(NSCalendarUnitMinute), 175 @"second": @(NSCalendarUnitSecond), 176 @"weekday": @(NSCalendarUnitWeekday), 177 @"weekOfMonth": @(NSCalendarUnitWeekOfMonth), 178 @"weekOfYear": @(NSCalendarUnitWeekOfYear), 179 @"weekdayOrdinal": @(NSCalendarUnitWeekdayOrdinal) 180 }; 181 } 182 return map; 183} 184 185- (NSArray<NSString *> *)automatchedDateComponentsKeys 186{ 187 return [[self dateComponentsMatchMap] allKeys]; 188} 189 190- (NSCalendarUnit)calendarUnitFor:(NSString *)key 191{ 192 return [[self dateComponentsMatchMap][key] unsignedIntegerValue]; 193} 194 195@end 196