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