1// Copyright 2018-present 650 Industries. All rights reserved.
2
3#import <EXNotifications/EXNotificationSerializer.h>
4#import <CoreLocation/CoreLocation.h>
5
6NS_ASSUME_NONNULL_BEGIN
7
8static NSString * const EXNotificationResponseDefaultActionIdentifier = @"expo.modules.notifications.actions.DEFAULT";
9
10@implementation EXNotificationSerializer
11
12+ (NSDictionary *)serializedNotificationResponse:(UNNotificationResponse *)response
13{
14  NSMutableDictionary *serializedResponse = [NSMutableDictionary dictionary];
15  NSString *actionIdentifier = response.actionIdentifier;
16  if ([UNNotificationDefaultActionIdentifier isEqualToString:actionIdentifier]) {
17    actionIdentifier = EXNotificationResponseDefaultActionIdentifier;
18  }
19  serializedResponse[@"actionIdentifier"] = actionIdentifier;
20  serializedResponse[@"notification"] = [self serializedNotification:response.notification];
21  if ([response isKindOfClass:[UNTextInputNotificationResponse class]]) {
22    UNTextInputNotificationResponse *textInputResponse = (UNTextInputNotificationResponse *)response;
23    serializedResponse[@"userText"] = textInputResponse.userText ?: [NSNull null];
24  }
25  return serializedResponse;
26}
27
28+ (NSDictionary *)serializedNotification:(UNNotification *)notification
29{
30  NSMutableDictionary *serializedNotification = [NSMutableDictionary dictionary];
31  serializedNotification[@"request"] = [self serializedNotificationRequest:notification.request];
32  serializedNotification[@"date"] = @(notification.date.timeIntervalSince1970);
33  return serializedNotification;
34}
35
36+ (NSDictionary *)serializedNotificationRequest:(UNNotificationRequest *)request
37{
38  NSMutableDictionary *serializedRequest = [NSMutableDictionary dictionary];
39  serializedRequest[@"identifier"] = request.identifier;
40  serializedRequest[@"content"] = [self serializedNotificationContent:request];
41  serializedRequest[@"trigger"] = [self serializedNotificationTrigger:request];
42  return serializedRequest;
43}
44
45+ (NSDictionary *)serializedNotificationContent:(UNNotificationRequest *)request
46{
47  UNNotificationContent *content = request.content;
48  NSMutableDictionary *serializedContent = [NSMutableDictionary dictionary];
49  serializedContent[@"title"] = content.title ?: [NSNull null];
50  serializedContent[@"subtitle"] = content.subtitle ?: [NSNull null];
51  serializedContent[@"body"] = content.body ?: [NSNull null];
52  serializedContent[@"badge"] = content.badge ?: [NSNull null];
53  serializedContent[@"sound"] = [self serializedNotificationSound:content.sound] ?: [NSNull null];
54  serializedContent[@"launchImageName"] = content.launchImageName ?: [NSNull null];
55  serializedContent[@"data"] = [self serializedNotificationData:request] ?: [NSNull null];
56  serializedContent[@"attachments"] = [self serializedNotificationAttachments:content.attachments];
57
58  if (@available(iOS 12.0, *)) {
59    serializedContent[@"summaryArgument"] = content.summaryArgument ?: [NSNull null];
60    serializedContent[@"summaryArgumentCount"] = @(content.summaryArgumentCount);
61  }
62  serializedContent[@"categoryIdentifier"] = content.categoryIdentifier ? content.categoryIdentifier : [NSNull null];
63  serializedContent[@"threadIdentifier"] = content.threadIdentifier ?: [NSNull null];
64  if (@available(iOS 13.0, *)) {
65    serializedContent[@"targetContentIdentifier"] = content.targetContentIdentifier ?: [NSNull null];
66  }
67
68  return serializedContent;
69}
70
71+ (NSDictionary *)serializedNotificationData:(UNNotificationRequest *)request
72{
73  BOOL isRemote = [request.trigger isKindOfClass:[UNPushNotificationTrigger class]];
74  return isRemote ? request.content.userInfo[@"body"] : request.content.userInfo;
75}
76
77+ (NSString *)serializedNotificationSound:(UNNotificationSound *)sound
78{
79  // nil compared to defaultCriticalSound returns true
80  if (!sound) {
81    return nil;
82  }
83
84  if (@available(iOS 12.0, *)) {
85    if ([[UNNotificationSound defaultCriticalSound] isEqual:sound]) {
86      return @"defaultCritical";
87    }
88  }
89
90  if ([[UNNotificationSound defaultSound] isEqual:sound]) {
91    return @"default";
92  }
93
94  return @"custom";
95}
96
97+ (NSArray *)serializedNotificationAttachments:(NSArray<UNNotificationAttachment *> *)attachments
98{
99  NSMutableArray *serializedAttachments = [NSMutableArray array];
100  for (UNNotificationAttachment *attachment in attachments) {
101    [serializedAttachments addObject:[self serializedNotificationAttachment:attachment]];
102  }
103  return serializedAttachments;
104}
105
106+ (NSDictionary *)serializedNotificationAttachment:(UNNotificationAttachment *)attachment
107{
108  NSMutableDictionary *serializedAttachment = [NSMutableDictionary dictionary];
109  serializedAttachment[@"identifier"] = attachment.identifier ?: [NSNull null];
110  serializedAttachment[@"url"] = attachment.URL.absoluteString ?: [NSNull null];
111  serializedAttachment[@"type"] = attachment.type ?: [NSNull null];
112  return serializedAttachment;
113}
114
115+ (NSDictionary *)serializedNotificationTrigger:(UNNotificationRequest *)request
116{
117  UNNotificationTrigger *trigger = request.trigger;
118  NSMutableDictionary *serializedTrigger = [NSMutableDictionary dictionary];
119  serializedTrigger[@"class"] = NSStringFromClass(trigger.class);
120  if ([trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
121    serializedTrigger[@"type"] = @"push";
122    serializedTrigger[@"payload"] = request.content.userInfo;
123  } else if ([trigger isKindOfClass:[UNCalendarNotificationTrigger class]]) {
124    serializedTrigger[@"type"] = @"calendar";
125    serializedTrigger[@"repeats"] = @(trigger.repeats);
126    UNCalendarNotificationTrigger *calendarTrigger = (UNCalendarNotificationTrigger *)trigger;
127    serializedTrigger[@"dateComponents"] = [self serializedDateComponents:calendarTrigger.dateComponents];
128#if !(TARGET_OS_MACCATALYST)
129  } else if ([trigger isKindOfClass:[UNLocationNotificationTrigger class]]) {
130    serializedTrigger[@"type"] = @"location";
131    serializedTrigger[@"repeats"] = @(trigger.repeats);
132    UNLocationNotificationTrigger *locationTrigger = (UNLocationNotificationTrigger *)trigger;
133    serializedTrigger[@"region"] = [self serializedRegion:locationTrigger.region];
134#endif
135  } else if ([trigger isKindOfClass:[UNTimeIntervalNotificationTrigger class]]) {
136    serializedTrigger[@"type"] = @"timeInterval";
137    UNTimeIntervalNotificationTrigger *timeIntervalTrigger = (UNTimeIntervalNotificationTrigger *)trigger;
138    serializedTrigger[@"seconds"] = @(timeIntervalTrigger.timeInterval);
139    serializedTrigger[@"repeats"] = @(trigger.repeats);
140  } else {
141    serializedTrigger[@"type"] = @"unknown";
142  }
143  return serializedTrigger;
144}
145
146+ (NSDictionary *)serializedDateComponents:(NSDateComponents *)dateComponents
147{
148  NSMutableDictionary *serializedComponents = [NSMutableDictionary dictionary];
149  NSArray<NSNumber *> *autoConvertedUnits = [[self calendarUnitsConversionMap] allKeys];
150  for (NSNumber *calendarUnitNumber in autoConvertedUnits) {
151    NSCalendarUnit calendarUnit = [calendarUnitNumber unsignedIntegerValue];
152    NSInteger unitValue = [dateComponents valueForComponent:calendarUnit];
153    if (unitValue != NSDateComponentUndefined) {
154      serializedComponents[[self keyForCalendarUnit:calendarUnit]] = @([dateComponents valueForComponent:calendarUnit]);
155    }
156  }
157  serializedComponents[@"calendar"] = dateComponents.calendar.calendarIdentifier ?: [NSNull null];
158  serializedComponents[@"timeZone"] = dateComponents.timeZone.description ?: [NSNull null];
159  serializedComponents[@"isLeapMonth"] = @(dateComponents.isLeapMonth);
160  return serializedComponents;
161}
162
163+ (NSDictionary *)calendarUnitsConversionMap
164{
165  static NSDictionary *keysMap = nil;
166  if (!keysMap) {
167    keysMap = @{
168      @(NSCalendarUnitEra): @"era",
169      @(NSCalendarUnitYear): @"year",
170      @(NSCalendarUnitMonth): @"month",
171      @(NSCalendarUnitDay): @"day",
172      @(NSCalendarUnitHour): @"hour",
173      @(NSCalendarUnitMinute): @"minute",
174      @(NSCalendarUnitSecond): @"second",
175      @(NSCalendarUnitWeekday): @"weekday",
176      @(NSCalendarUnitWeekdayOrdinal): @"weekdayOrdinal",
177      @(NSCalendarUnitQuarter): @"quarter",
178      @(NSCalendarUnitWeekOfMonth): @"weekOfMonth",
179      @(NSCalendarUnitWeekOfYear): @"weekOfYear",
180      @(NSCalendarUnitYearForWeekOfYear): @"yearForWeekOfYear",
181      @(NSCalendarUnitNanosecond): @"nanosecond"
182      // NSCalendarUnitCalendar and NSCalendarUnitTimeZone
183      // should be handled separately
184    };
185  }
186  return keysMap;
187}
188
189+ (NSString *)keyForCalendarUnit:(NSCalendarUnit)calendarUnit
190{
191  return [self calendarUnitsConversionMap][@(calendarUnit)];
192}
193
194+ (NSDictionary *)serializedRegion:(CLRegion *)region
195{
196  NSMutableDictionary *serializedRegion = [NSMutableDictionary dictionary];
197  serializedRegion[@"identifier"] = region.identifier;
198  serializedRegion[@"notifyOnEntry"] = @(region.notifyOnEntry);
199  serializedRegion[@"notifyOnExit"] = @(region.notifyOnExit);
200  if ([region isKindOfClass:[CLCircularRegion class]]) {
201    serializedRegion[@"type"] = @"circular";
202    CLCircularRegion *circularRegion = (CLCircularRegion *)region;
203    NSDictionary *serializedCenter = @{
204      @"latitude": @(circularRegion.center.latitude),
205      @"longitude": @(circularRegion.center.longitude)
206    };
207    serializedRegion[@"center"] = serializedCenter;
208    serializedRegion[@"radius"] = @(circularRegion.radius);
209  } else if ([region isKindOfClass:[CLBeaconRegion class]]) {
210    serializedRegion[@"type"] = @"beacon";
211    CLBeaconRegion *beaconRegion = (CLBeaconRegion *)region;
212    serializedRegion[@"notifyEntryStateOnDisplay"] = @(beaconRegion.notifyEntryStateOnDisplay);
213    serializedRegion[@"major"] = beaconRegion.major ?: [NSNull null];
214    serializedRegion[@"minor"] = beaconRegion.minor ?: [NSNull null];
215    if (@available(iOS 13.0, *)) {
216      serializedRegion[@"uuid"] = beaconRegion.UUID;
217      NSDictionary *serializedConstraint = @{
218        @"uuid": beaconRegion.beaconIdentityConstraint.UUID,
219        @"major": beaconRegion.beaconIdentityConstraint.major ?: [NSNull null],
220        @"minor": beaconRegion.beaconIdentityConstraint.minor ?: [NSNull null],
221      };
222      serializedRegion[@"beaconIdentityConstraint"] = serializedConstraint;
223    }
224  } else {
225    serializedRegion[@"type"] = @"unknown";
226  }
227  return serializedRegion;
228}
229
230@end
231
232NS_ASSUME_NONNULL_END
233