From a4f98e18a2f8a54321def267f2ff32014997ccc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Fri, 20 Sep 2024 13:04:04 +0200 Subject: [PATCH] Add iOS notification service support (#6) --- apple/BareKit/BareKit.h | 18 ++++++ apple/BareKit/BareKit.m | 123 ++++++++++++++++++++++++++++++++++++++++ apple/CMakeLists.txt | 1 + shared/worklet.js | 12 +++- 4 files changed, 152 insertions(+), 2 deletions(-) diff --git a/apple/BareKit/BareKit.h b/apple/BareKit/BareKit.h index 928ee1b..bd6eeec 100644 --- a/apple/BareKit/BareKit.h +++ b/apple/BareKit/BareKit.h @@ -4,6 +4,7 @@ #if defined(__OBJC__) #import +#import @interface BareWorklet : NSObject @@ -92,6 +93,23 @@ typedef void (^BareRPCResponseHandler)(NSData *_Nullable data, NSError *_Nullabl @end +@interface BareNotificationService : UNNotificationServiceExtension + +- (_Nullable instancetype)initWithFilename:(NSString *_Nonnull)filename + source:(NSData *_Nonnull)source; +- (_Nullable instancetype)initWithFilename:(NSString *_Nonnull)filename + source:(NSString *_Nonnull)source + encoding:(NSStringEncoding)encoding; +- (_Nullable instancetype)initWithResource:(NSString *_Nonnull)name + ofType:(NSString *_Nonnull)type + inBundle:(NSBundle *_Nonnull)bundle; +- (_Nullable instancetype)initWithResource:(NSString *_Nonnull)name + ofType:(NSString *_Nonnull)type + inDirectory:(NSString *_Nonnull)subpath + inBundle:(NSBundle *_Nonnull)bundle; + +@end + #endif #endif // BARE_KIT_H diff --git a/apple/BareKit/BareKit.m b/apple/BareKit/BareKit.m index 0909318..723458e 100644 --- a/apple/BareKit/BareKit.m +++ b/apple/BareKit/BareKit.m @@ -1,4 +1,5 @@ #import +#import #import #import @@ -552,3 +553,125 @@ - (void)_reply:(BareRPCIncomingRequest *_Nonnull)request } @end + +@implementation BareNotificationService { + BareWorklet *_worklet; +} + +- (_Nullable instancetype)initWithFilename:(NSString *_Nonnull)filename + source:(NSData *_Nonnull)source { + self = [super init]; + + if (self) { + [BareWorklet optimizeForMemory:YES]; + + _worklet = [[BareWorklet alloc] init]; + + [_worklet start:filename source:source]; + } + + return self; +} + +- (_Nullable instancetype)initWithFilename:(NSString *_Nonnull)filename + source:(NSString *_Nonnull)source + encoding:(NSStringEncoding)encoding { + return [self initWithFilename:filename + source:[source dataUsingEncoding:encoding]]; +} + +- (_Nullable instancetype)initWithResource:(NSString *_Nonnull)name + ofType:(NSString *_Nonnull)type + inBundle:(NSBundle *_Nonnull)bundle { + NSString *path = [bundle pathForResource:name ofType:type]; + + return [self initWithFilename:path + source:[NSData dataWithContentsOfFile:path]]; +} + +- (_Nullable instancetype)initWithResource:(NSString *_Nonnull)name + ofType:(NSString *_Nonnull)type + inDirectory:(NSString *_Nonnull)subpath + inBundle:(NSBundle *_Nonnull)bundle { + NSString *path = [bundle pathForResource:name ofType:type inDirectory:subpath]; + + return [self initWithFilename:path + source:[NSData dataWithContentsOfFile:path]]; +} + +- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request + withContentHandler:(void (^)(UNNotificationContent *_Nonnull))contentHandler { + NSError *error; + + NSData *json = [NSJSONSerialization dataWithJSONObject:request.content.userInfo options:0 error:&error]; + + assert(error == nil); // `userInfo` has already been validated + + [_worklet push:json + completion:^(NSData *_Nullable reply, NSError *_Nullable error) { + UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; + + if (reply) { + NSError *error; + + id data = [NSJSONSerialization JSONObjectWithData:reply options:0 error:&error]; + + if (data) { + // Primary content + content.title = data[@"title"]; + content.subtitle = data[@"subtitle"]; + content.body = data[@"body"]; + + // Supplementary content + content.userInfo = data[@"userInfo"]; + + // App behavior + content.badge = data[@"badge"]; + content.targetContentIdentifier = data[@"targetContentIdentifier"]; + + // System integration + id sound = data[@"sound"]; + + if (sound) { + id type = sound[@"type"]; + id critical = sound[@"critical"]; + id volume = sound[@"volume"]; + + if ([type isEqualToString:@"default"]) { + if (critical) { + if (volume) { + content.sound = [UNNotificationSound defaultCriticalSoundWithAudioVolume:[volume floatValue]]; + } else { + content.sound = [UNNotificationSound defaultCriticalSound]; + } + } else { + content.sound = [UNNotificationSound defaultSound]; + } + } else if ([type isEqualToString:@"named"]) { + if (critical) { + if (volume) { + content.sound = [UNNotificationSound criticalSoundNamed:sound[@"name"] withAudioVolume:[volume floatValue]]; + } else { + content.sound = [UNNotificationSound criticalSoundNamed:sound[@"name"]]; + } + } else { + content.sound = [UNNotificationSound soundNamed:sound[@"name"]]; + } + } + } + + // Grouping + content.threadIdentifier = data[@"threadIdentifier"]; + content.categoryIdentifier = data[@"categoryIdentifier"]; + } + } + + contentHandler(content); + }]; +} + +- (void)serviceExtensionTimeWillExpire { + [_worklet suspend]; +} + +@end diff --git a/apple/CMakeLists.txt b/apple/CMakeLists.txt index 61b5fbc..bf40785 100644 --- a/apple/CMakeLists.txt +++ b/apple/CMakeLists.txt @@ -31,6 +31,7 @@ target_link_libraries( bare_kit PUBLIC "-framework Foundation" + "-framework UserNotifications" PUBLIC bare_worklet PRIVATE diff --git a/shared/worklet.js b/shared/worklet.js index c144846..ff8176b 100644 --- a/shared/worklet.js +++ b/shared/worklet.js @@ -29,8 +29,14 @@ exports.BareKit = new BareKit() exports.port = ports[1] exports.push = function push (payload, reply) { - if (exports.BareKit.emit('push', Buffer.from(payload), reply) === false) { - reply(null, null) + if (exports.BareKit.emit('push', Buffer.from(payload), replyOnce) === false) { + replyOnce(null, null) + } + + function replyOnce (err, payload, encoding) { + reply(err, typeof payload === 'string' ? Buffer.from(payload, encoding) : payload) + + reply = noop } } @@ -40,3 +46,5 @@ Object.defineProperty(global, 'BareKit', { writable: false, configurable: true }) + +function noop () {}