- App.tsx: full navigation (Auth stack + Main tabs with 5 screens) - Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen - HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors - MapScreen: interactive map with layer toggles (6 layers) - MarketplaceScreen: categories (6), products (5), search - ChatScreen: AI chat with quick prompts (4), bot responses - ProfileScreen: user info, stats, menu (9 items), logout - AlertsScreen: alert list with severity, acknowledge - SensorsScreen: sensor list with type filters (6 types), search - ZonesScreen: zone cards with stats - SettingsScreen: language picker (FR/EN/ES/DE), privacy, about - Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n - Hooks: useSensors, useAlerts, useNotifications, useLocation - Components: Card, Button, LoadingSpinner, ErrorBoundary, Header - Services: iotService, notificationService (with axios API client) - Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN) - Theme: colors.ts with full design system (Blue Ocean palette) - Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
250 lines
11 KiB
Objective-C
250 lines
11 KiB
Objective-C
// Copyright 2018-present 650 Industries. All rights reserved.
|
|
|
|
#import <EXNotifications/EXNotificationSerializer.h>
|
|
#import <CoreLocation/CoreLocation.h>
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
static NSString * const EXNotificationResponseDefaultActionIdentifier = @"expo.modules.notifications.actions.DEFAULT";
|
|
|
|
@implementation EXNotificationSerializer
|
|
|
|
+ (NSDictionary *)serializedNotificationResponse:(UNNotificationResponse *)response
|
|
{
|
|
NSMutableDictionary *serializedResponse = [NSMutableDictionary dictionary];
|
|
NSString *actionIdentifier = response.actionIdentifier;
|
|
if ([UNNotificationDefaultActionIdentifier isEqualToString:actionIdentifier]) {
|
|
actionIdentifier = EXNotificationResponseDefaultActionIdentifier;
|
|
}
|
|
serializedResponse[@"actionIdentifier"] = actionIdentifier;
|
|
serializedResponse[@"notification"] = [self serializedNotification:response.notification];
|
|
if ([response isKindOfClass:[UNTextInputNotificationResponse class]]) {
|
|
UNTextInputNotificationResponse *textInputResponse = (UNTextInputNotificationResponse *)response;
|
|
serializedResponse[@"userText"] = textInputResponse.userText ?: [NSNull null];
|
|
}
|
|
return serializedResponse;
|
|
}
|
|
|
|
+ (NSDictionary *)serializedNotification:(UNNotification *)notification
|
|
{
|
|
NSMutableDictionary *serializedNotification = [NSMutableDictionary dictionary];
|
|
serializedNotification[@"request"] = [self serializedNotificationRequest:notification.request];
|
|
serializedNotification[@"date"] = @(notification.date.timeIntervalSince1970);
|
|
return serializedNotification;
|
|
}
|
|
|
|
+ (NSDictionary *)serializedNotificationRequest:(UNNotificationRequest *)request
|
|
{
|
|
NSMutableDictionary *serializedRequest = [NSMutableDictionary dictionary];
|
|
serializedRequest[@"identifier"] = request.identifier;
|
|
serializedRequest[@"content"] = [self serializedNotificationContent:request];
|
|
serializedRequest[@"trigger"] = [self serializedNotificationTrigger:request];
|
|
return serializedRequest;
|
|
}
|
|
|
|
+ (NSDictionary *)serializedNotificationContent:(UNNotificationRequest *)request
|
|
{
|
|
UNNotificationContent *content = request.content;
|
|
NSMutableDictionary *serializedContent = [NSMutableDictionary dictionary];
|
|
serializedContent[@"title"] = content.title ?: [NSNull null];
|
|
serializedContent[@"subtitle"] = content.subtitle ?: [NSNull null];
|
|
serializedContent[@"body"] = content.body ?: [NSNull null];
|
|
serializedContent[@"badge"] = content.badge ?: [NSNull null];
|
|
serializedContent[@"sound"] = [self serializedNotificationSound:content.sound] ?: [NSNull null];
|
|
serializedContent[@"launchImageName"] = content.launchImageName ?: [NSNull null];
|
|
serializedContent[@"data"] = [self serializedNotificationData:request] ?: [NSNull null];
|
|
serializedContent[@"attachments"] = [self serializedNotificationAttachments:content.attachments];
|
|
|
|
if (@available(iOS 12.0, *)) {
|
|
serializedContent[@"summaryArgument"] = content.summaryArgument ?: [NSNull null];
|
|
serializedContent[@"summaryArgumentCount"] = @(content.summaryArgumentCount);
|
|
}
|
|
serializedContent[@"categoryIdentifier"] = content.categoryIdentifier ? content.categoryIdentifier : [NSNull null];
|
|
serializedContent[@"threadIdentifier"] = content.threadIdentifier ?: [NSNull null];
|
|
if (@available(iOS 13.0, *)) {
|
|
serializedContent[@"targetContentIdentifier"] = content.targetContentIdentifier ?: [NSNull null];
|
|
}
|
|
if (@available(iOS 15.0, *)) {
|
|
serializedContent[@"interruptionLevel"] = [EXNotificationSerializer serializedInterruptionLevel:content.interruptionLevel];
|
|
}
|
|
|
|
return serializedContent;
|
|
}
|
|
|
|
+ (NSString *)serializedInterruptionLevel:(UNNotificationInterruptionLevel)interruptionLevel API_AVAILABLE(ios(15.0)) {
|
|
static NSDictionary<NSNumber *, NSString *> *interruptionLevelMap;
|
|
if (!interruptionLevelMap) {
|
|
interruptionLevelMap = @{
|
|
@(UNNotificationInterruptionLevelPassive): @"passive",
|
|
@(UNNotificationInterruptionLevelActive): @"active",
|
|
@(UNNotificationInterruptionLevelTimeSensitive): @"timeSensitive",
|
|
@(UNNotificationInterruptionLevelCritical): @"critical"
|
|
};
|
|
}
|
|
|
|
return [interruptionLevelMap objectForKey:@(interruptionLevel)];
|
|
}
|
|
|
|
+ (NSDictionary *)serializedNotificationData:(UNNotificationRequest *)request
|
|
{
|
|
BOOL isRemote = [request.trigger isKindOfClass:[UNPushNotificationTrigger class]];
|
|
return isRemote ? request.content.userInfo[@"body"] : request.content.userInfo;
|
|
}
|
|
|
|
+ (NSString *)serializedNotificationSound:(UNNotificationSound *)sound
|
|
{
|
|
// nil compared to defaultCriticalSound returns true
|
|
if (!sound) {
|
|
return nil;
|
|
}
|
|
|
|
if (@available(iOS 12.0, *)) {
|
|
if ([[UNNotificationSound defaultCriticalSound] isEqual:sound]) {
|
|
return @"defaultCritical";
|
|
}
|
|
}
|
|
|
|
if ([[UNNotificationSound defaultSound] isEqual:sound]) {
|
|
return @"default";
|
|
}
|
|
|
|
return @"custom";
|
|
}
|
|
|
|
+ (NSArray *)serializedNotificationAttachments:(NSArray<UNNotificationAttachment *> *)attachments
|
|
{
|
|
NSMutableArray *serializedAttachments = [NSMutableArray array];
|
|
for (UNNotificationAttachment *attachment in attachments) {
|
|
[serializedAttachments addObject:[self serializedNotificationAttachment:attachment]];
|
|
}
|
|
return serializedAttachments;
|
|
}
|
|
|
|
+ (NSDictionary *)serializedNotificationAttachment:(UNNotificationAttachment *)attachment
|
|
{
|
|
NSMutableDictionary *serializedAttachment = [NSMutableDictionary dictionary];
|
|
serializedAttachment[@"identifier"] = attachment.identifier ?: [NSNull null];
|
|
serializedAttachment[@"url"] = attachment.URL.absoluteString ?: [NSNull null];
|
|
serializedAttachment[@"type"] = attachment.type ?: [NSNull null];
|
|
return serializedAttachment;
|
|
}
|
|
|
|
+ (NSDictionary *)serializedNotificationTrigger:(UNNotificationRequest *)request
|
|
{
|
|
UNNotificationTrigger *trigger = request.trigger;
|
|
NSMutableDictionary *serializedTrigger = [NSMutableDictionary dictionary];
|
|
serializedTrigger[@"class"] = NSStringFromClass(trigger.class);
|
|
if ([trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
|
|
serializedTrigger[@"type"] = @"push";
|
|
serializedTrigger[@"payload"] = request.content.userInfo;
|
|
} else if ([trigger isKindOfClass:[UNCalendarNotificationTrigger class]]) {
|
|
serializedTrigger[@"type"] = @"calendar";
|
|
serializedTrigger[@"repeats"] = @(trigger.repeats);
|
|
UNCalendarNotificationTrigger *calendarTrigger = (UNCalendarNotificationTrigger *)trigger;
|
|
serializedTrigger[@"dateComponents"] = [self serializedDateComponents:calendarTrigger.dateComponents];
|
|
#if !(TARGET_OS_MACCATALYST)
|
|
} else if ([trigger isKindOfClass:[UNLocationNotificationTrigger class]]) {
|
|
serializedTrigger[@"type"] = @"location";
|
|
serializedTrigger[@"repeats"] = @(trigger.repeats);
|
|
UNLocationNotificationTrigger *locationTrigger = (UNLocationNotificationTrigger *)trigger;
|
|
serializedTrigger[@"region"] = [self serializedRegion:locationTrigger.region];
|
|
#endif
|
|
} else if ([trigger isKindOfClass:[UNTimeIntervalNotificationTrigger class]]) {
|
|
serializedTrigger[@"type"] = @"timeInterval";
|
|
UNTimeIntervalNotificationTrigger *timeIntervalTrigger = (UNTimeIntervalNotificationTrigger *)trigger;
|
|
serializedTrigger[@"seconds"] = @(timeIntervalTrigger.timeInterval);
|
|
serializedTrigger[@"repeats"] = @(trigger.repeats);
|
|
} else {
|
|
serializedTrigger[@"type"] = @"unknown";
|
|
}
|
|
return serializedTrigger;
|
|
}
|
|
|
|
+ (NSDictionary *)serializedDateComponents:(NSDateComponents *)dateComponents
|
|
{
|
|
NSMutableDictionary *serializedComponents = [NSMutableDictionary dictionary];
|
|
NSArray<NSNumber *> *autoConvertedUnits = [[self calendarUnitsConversionMap] allKeys];
|
|
for (NSNumber *calendarUnitNumber in autoConvertedUnits) {
|
|
NSCalendarUnit calendarUnit = [calendarUnitNumber unsignedIntegerValue];
|
|
NSInteger unitValue = [dateComponents valueForComponent:calendarUnit];
|
|
if (unitValue != NSDateComponentUndefined) {
|
|
serializedComponents[[self keyForCalendarUnit:calendarUnit]] = @([dateComponents valueForComponent:calendarUnit]);
|
|
}
|
|
}
|
|
serializedComponents[@"calendar"] = dateComponents.calendar.calendarIdentifier ?: [NSNull null];
|
|
serializedComponents[@"timeZone"] = dateComponents.timeZone.description ?: [NSNull null];
|
|
serializedComponents[@"isLeapMonth"] = @(dateComponents.isLeapMonth);
|
|
return serializedComponents;
|
|
}
|
|
|
|
+ (NSDictionary *)calendarUnitsConversionMap
|
|
{
|
|
static NSDictionary *keysMap = nil;
|
|
if (!keysMap) {
|
|
keysMap = @{
|
|
@(NSCalendarUnitEra): @"era",
|
|
@(NSCalendarUnitYear): @"year",
|
|
@(NSCalendarUnitMonth): @"month",
|
|
@(NSCalendarUnitDay): @"day",
|
|
@(NSCalendarUnitHour): @"hour",
|
|
@(NSCalendarUnitMinute): @"minute",
|
|
@(NSCalendarUnitSecond): @"second",
|
|
@(NSCalendarUnitWeekday): @"weekday",
|
|
@(NSCalendarUnitWeekdayOrdinal): @"weekdayOrdinal",
|
|
@(NSCalendarUnitQuarter): @"quarter",
|
|
@(NSCalendarUnitWeekOfMonth): @"weekOfMonth",
|
|
@(NSCalendarUnitWeekOfYear): @"weekOfYear",
|
|
@(NSCalendarUnitYearForWeekOfYear): @"yearForWeekOfYear",
|
|
@(NSCalendarUnitNanosecond): @"nanosecond"
|
|
// NSCalendarUnitCalendar and NSCalendarUnitTimeZone
|
|
// should be handled separately
|
|
};
|
|
}
|
|
return keysMap;
|
|
}
|
|
|
|
+ (NSString *)keyForCalendarUnit:(NSCalendarUnit)calendarUnit
|
|
{
|
|
return [self calendarUnitsConversionMap][@(calendarUnit)];
|
|
}
|
|
|
|
+ (NSDictionary *)serializedRegion:(CLRegion *)region
|
|
{
|
|
NSMutableDictionary *serializedRegion = [NSMutableDictionary dictionary];
|
|
serializedRegion[@"identifier"] = region.identifier;
|
|
serializedRegion[@"notifyOnEntry"] = @(region.notifyOnEntry);
|
|
serializedRegion[@"notifyOnExit"] = @(region.notifyOnExit);
|
|
if ([region isKindOfClass:[CLCircularRegion class]]) {
|
|
serializedRegion[@"type"] = @"circular";
|
|
CLCircularRegion *circularRegion = (CLCircularRegion *)region;
|
|
NSDictionary *serializedCenter = @{
|
|
@"latitude": @(circularRegion.center.latitude),
|
|
@"longitude": @(circularRegion.center.longitude)
|
|
};
|
|
serializedRegion[@"center"] = serializedCenter;
|
|
serializedRegion[@"radius"] = @(circularRegion.radius);
|
|
} else if ([region isKindOfClass:[CLBeaconRegion class]]) {
|
|
serializedRegion[@"type"] = @"beacon";
|
|
CLBeaconRegion *beaconRegion = (CLBeaconRegion *)region;
|
|
serializedRegion[@"notifyEntryStateOnDisplay"] = @(beaconRegion.notifyEntryStateOnDisplay);
|
|
serializedRegion[@"major"] = beaconRegion.major ?: [NSNull null];
|
|
serializedRegion[@"minor"] = beaconRegion.minor ?: [NSNull null];
|
|
if (@available(iOS 13.0, *)) {
|
|
serializedRegion[@"uuid"] = beaconRegion.UUID;
|
|
NSDictionary *serializedConstraint = @{
|
|
@"uuid": beaconRegion.beaconIdentityConstraint.UUID,
|
|
@"major": beaconRegion.beaconIdentityConstraint.major ?: [NSNull null],
|
|
@"minor": beaconRegion.beaconIdentityConstraint.minor ?: [NSNull null],
|
|
};
|
|
serializedRegion[@"beaconIdentityConstraint"] = serializedConstraint;
|
|
}
|
|
} else {
|
|
serializedRegion[@"type"] = @"unknown";
|
|
}
|
|
return serializedRegion;
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|