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