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 weeklyNotificationTriggerType = @"weekly"; 21static NSString * const weeklyNotificationTriggerWeekdayKey = @"weekday"; 22static NSString * const weeklyNotificationTriggerHourKey = @"hour"; 23static NSString * const weeklyNotificationTriggerMinuteKey = @"minute"; 24 25static NSString * const yearlyNotificationTriggerType = @"yearly"; 26static NSString * const yearlyNotificationTriggerDayKey = @"day"; 27static NSString * const yearlyNotificationTriggerMonthKey = @"month"; 28static NSString * const yearlyNotificationTriggerHourKey = @"hour"; 29static NSString * const yearlyNotificationTriggerMinuteKey = @"minute"; 30 31static NSString * const dateNotificationTriggerType = @"date"; 32static NSString * const dateNotificationTriggerTimestampKey = @"timestamp"; 33 34static NSString * const calendarNotificationTriggerType = @"calendar"; 35static NSString * const calendarNotificationTriggerComponentsKey = @"value"; 36static NSString * const calendarNotificationTriggerTimezoneKey = @"timezone"; 37 38 39 40@interface EXNotificationSchedulerModule () 41 42@property (nonatomic, weak) id<EXNotificationBuilder> builder; 43 44@end 45 46@implementation EXNotificationSchedulerModule 47 48EX_EXPORT_MODULE(ExpoNotificationScheduler); 49 50- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry 51{ 52 _builder = [moduleRegistry getModuleImplementingProtocol:@protocol(EXNotificationBuilder)]; 53} 54 55# pragma mark - Exported methods 56 57EX_EXPORT_METHOD_AS(getAllScheduledNotificationsAsync, 58 getAllScheduledNotifications:(EXPromiseResolveBlock)resolve reject:(EXPromiseRejectBlock)reject 59 ) 60{ 61 [[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:^(NSArray<UNNotificationRequest *> * _Nonnull requests) { 62 resolve([self serializeNotificationRequests:requests]); 63 }]; 64} 65 66EX_EXPORT_METHOD_AS(scheduleNotificationAsync, 67 scheduleNotification:(NSString *)identifier notificationSpec:(NSDictionary *)notificationSpec triggerSpec:(NSDictionary *)triggerSpec resolve:(EXPromiseResolveBlock)resolve rejecting:(EXPromiseRejectBlock)reject) 68{ 69 @try { 70 UNNotificationRequest *request = [self buildNotificationRequestWithIdentifier:identifier content:notificationSpec trigger:triggerSpec]; 71 [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { 72 if (error) { 73 NSString *message = [NSString stringWithFormat:@"Failed to schedule notification. %@", error]; 74 reject(@"ERR_NOTIFICATIONS_FAILED_TO_SCHEDULE", message, error); 75 } else { 76 resolve(identifier); 77 } 78 }]; 79 } @catch (NSException *exception) { 80 NSString *message = [NSString stringWithFormat:@"Failed to schedule notification. %@", exception]; 81 reject(@"ERR_NOTIFICATIONS_FAILED_TO_SCHEDULE", message, nil); 82 } 83} 84 85EX_EXPORT_METHOD_AS(cancelScheduledNotificationAsync, 86 cancelNotification:(NSString *)identifier resolve:(EXPromiseResolveBlock)resolve rejecting:(EXPromiseRejectBlock)reject) 87{ 88 [[UNUserNotificationCenter currentNotificationCenter] removePendingNotificationRequestsWithIdentifiers:@[identifier]]; 89 resolve(nil); 90} 91 92EX_EXPORT_METHOD_AS(cancelAllScheduledNotificationsAsync, 93 cancelAllNotificationsWithResolver:(EXPromiseResolveBlock)resolve rejecting:(EXPromiseRejectBlock)reject) 94{ 95 [[UNUserNotificationCenter currentNotificationCenter] removeAllPendingNotificationRequests]; 96 resolve(nil); 97} 98 99EX_EXPORT_METHOD_AS(getNextTriggerDateAsync, 100 getNextTriggerDate:(NSDictionary *)triggerSpec resolve:(EXPromiseResolveBlock)resolve rejecting:(EXPromiseRejectBlock)reject) 101{ 102 @try { 103 UNNotificationTrigger *trigger = [self triggerFromParams:triggerSpec]; 104 if ([trigger isKindOfClass:[UNCalendarNotificationTrigger class]]) { 105 UNCalendarNotificationTrigger *calendarTrigger = (UNCalendarNotificationTrigger *)trigger; 106 NSDate *nextTriggerDate = [calendarTrigger nextTriggerDate]; 107 // We want to return milliseconds from this method. 108 resolve(nextTriggerDate ? @([nextTriggerDate timeIntervalSince1970] * 1000) : [NSNull null]); 109 } else if ([trigger isKindOfClass:[UNTimeIntervalNotificationTrigger class]]) { 110 UNTimeIntervalNotificationTrigger *timeIntervalTrigger = (UNTimeIntervalNotificationTrigger *)trigger; 111 NSDate *nextTriggerDate = [timeIntervalTrigger nextTriggerDate]; 112 // We want to return milliseconds from this method. 113 resolve(nextTriggerDate ? @([nextTriggerDate timeIntervalSince1970] * 1000) : [NSNull null]); 114 } else { 115 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])]; 116 reject(@"ERR_NOTIFICATIONS_INVALID_CALENDAR_TRIGGER", message, nil); 117 } 118 } @catch (NSException *exception) { 119 NSString *message = [NSString stringWithFormat:@"Failed to get next trigger date. %@", exception]; 120 reject(@"ERR_NOTIFICATIONS_FAILED_TO_GET_NEXT_TRIGGER_DATE", message, nil); 121 } 122} 123 124- (UNNotificationRequest *)buildNotificationRequestWithIdentifier:(NSString *)identifier 125 content:(NSDictionary *)contentInput 126 trigger:(NSDictionary *)triggerInput 127{ 128 UNNotificationContent *content = [_builder notificationContentFromRequest:contentInput]; 129 UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:[self triggerFromParams:triggerInput]]; 130 return request; 131} 132 133- (NSArray * _Nonnull)serializeNotificationRequests:(NSArray<UNNotificationRequest *> * _Nonnull) requests 134{ 135 NSMutableArray *serializedRequests = [NSMutableArray new]; 136 for (UNNotificationRequest *request in requests) { 137 [serializedRequests addObject:[EXNotificationSerializer serializedNotificationRequest:request]]; 138 } 139 return serializedRequests; 140} 141 142- (UNNotificationTrigger *)triggerFromParams:(NSDictionary *)params 143{ 144 if (!params) { 145 // nil trigger is a valid trigger 146 return nil; 147 } 148 if (![params isKindOfClass:[NSDictionary class]]) { 149 NSString *reason = [NSString stringWithFormat:@"Unknown notification trigger declaration passed in, expected a dictionary, received %@.", NSStringFromClass(params.class)]; 150 @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; 151 } 152 NSString *triggerType = params[notificationTriggerTypeKey]; 153 if ([intervalNotificationTriggerType isEqualToString:triggerType]) { 154 NSNumber *interval = [params objectForKey:intervalNotificationTriggerIntervalKey verifyingClass:[NSNumber class]]; 155 NSNumber *repeats = [params objectForKey:notificationTriggerRepeatsKey verifyingClass:[NSNumber class]]; 156 157 return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:[interval unsignedIntegerValue] 158 repeats:[repeats boolValue]]; 159 } else if ([dateNotificationTriggerType isEqualToString:triggerType]) { 160 NSNumber *timestampMs = [params objectForKey:dateNotificationTriggerTimestampKey verifyingClass:[NSNumber class]]; 161 NSUInteger timestamp = [timestampMs unsignedIntegerValue] / 1000; 162 NSDate *date = [NSDate dateWithTimeIntervalSince1970:timestamp]; 163 164 return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:[date timeIntervalSinceNow] 165 repeats:NO]; 166 167 } else if ([dailyNotificationTriggerType isEqualToString:triggerType]) { 168 NSNumber *hour = [params objectForKey:dailyNotificationTriggerHourKey verifyingClass:[NSNumber class]]; 169 NSNumber *minute = [params objectForKey:dailyNotificationTriggerMinuteKey verifyingClass:[NSNumber class]]; 170 NSDateComponents *dateComponents = [NSDateComponents new]; 171 dateComponents.hour = [hour integerValue]; 172 dateComponents.minute = [minute integerValue]; 173 174 return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents 175 repeats:YES]; 176 } else if ([weeklyNotificationTriggerType isEqualToString:triggerType]) { 177 NSNumber *weekday = [params objectForKey:weeklyNotificationTriggerWeekdayKey verifyingClass:[NSNumber class]]; 178 NSNumber *hour = [params objectForKey:weeklyNotificationTriggerHourKey verifyingClass:[NSNumber class]]; 179 NSNumber *minute = [params objectForKey:weeklyNotificationTriggerMinuteKey verifyingClass:[NSNumber class]]; 180 NSDateComponents *dateComponents = [NSDateComponents new]; 181 dateComponents.weekday = [weekday integerValue]; 182 dateComponents.hour = [hour integerValue]; 183 dateComponents.minute = [minute integerValue]; 184 185 return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents 186 repeats:YES]; 187 } else if ([yearlyNotificationTriggerType isEqualToString:triggerType]) { 188 NSNumber *day = [params objectForKey:yearlyNotificationTriggerDayKey verifyingClass:[NSNumber class]]; 189 NSNumber *month = [params objectForKey:yearlyNotificationTriggerMonthKey verifyingClass:[NSNumber class]]; 190 NSNumber *hour = [params objectForKey:yearlyNotificationTriggerHourKey verifyingClass:[NSNumber class]]; 191 NSNumber *minute = [params objectForKey:yearlyNotificationTriggerMinuteKey verifyingClass:[NSNumber class]]; 192 NSDateComponents *dateComponents = [NSDateComponents new]; 193 dateComponents.day = [day integerValue]; 194 dateComponents.month = [month integerValue] + 1; // iOS uses 1-12 based numbers for months 195 dateComponents.hour = [hour integerValue]; 196 dateComponents.minute = [minute integerValue]; 197 198 return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents 199 repeats:YES]; 200 } else if ([calendarNotificationTriggerType isEqualToString:triggerType]) { 201 NSDateComponents *dateComponents = [self dateComponentsFromParams:params[calendarNotificationTriggerComponentsKey]]; 202 NSNumber *repeats = [params objectForKey:notificationTriggerRepeatsKey verifyingClass:[NSNumber class]]; 203 204 return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents 205 repeats:[repeats boolValue]]; 206 } else { 207 NSString *reason = [NSString stringWithFormat:@"Unknown notification trigger type: %@.", triggerType]; 208 @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; 209 } 210} 211 212- (NSDateComponents *)dateComponentsFromParams:(NSDictionary<NSString *, id> *)params 213{ 214 NSDateComponents *dateComponents = [NSDateComponents new]; 215 216 // TODO: Verify that DoW matches JS getDay() 217 dateComponents.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601]; 218 219 if ([params objectForKey:calendarNotificationTriggerTimezoneKey verifyingClass:[NSString class]]) { 220 dateComponents.timeZone = [[NSTimeZone alloc] initWithName:params[calendarNotificationTriggerTimezoneKey]]; 221 } 222 223 for (NSString *key in [self automatchedDateComponentsKeys]) { 224 if (params[key]) { 225 NSNumber *value = [params objectForKey:key verifyingClass:[NSNumber class]]; 226 [dateComponents setValue:[value unsignedIntegerValue] forComponent:[self calendarUnitFor:key]]; 227 } 228 } 229 230 return dateComponents; 231} 232 233- (NSDictionary<NSString *, NSNumber *> *)dateComponentsMatchMap 234{ 235 static NSDictionary *map; 236 if (!map) { 237 map = @{ 238 @"year": @(NSCalendarUnitYear), 239 @"month": @(NSCalendarUnitMonth), 240 @"day": @(NSCalendarUnitDay), 241 @"hour": @(NSCalendarUnitHour), 242 @"minute": @(NSCalendarUnitMinute), 243 @"second": @(NSCalendarUnitSecond), 244 @"weekday": @(NSCalendarUnitWeekday), 245 @"weekOfMonth": @(NSCalendarUnitWeekOfMonth), 246 @"weekOfYear": @(NSCalendarUnitWeekOfYear), 247 @"weekdayOrdinal": @(NSCalendarUnitWeekdayOrdinal) 248 }; 249 } 250 return map; 251} 252 253- (NSArray<NSString *> *)automatchedDateComponentsKeys 254{ 255 return [[self dateComponentsMatchMap] allKeys]; 256} 257 258- (NSCalendarUnit)calendarUnitFor:(NSString *)key 259{ 260 return [[self dateComponentsMatchMap][key] unsignedIntegerValue]; 261} 262 263@end 264