diff --git a/.csharp.source/req.http b/.csharp.source/req.http index 9184c42..e96e377 100644 --- a/.csharp.source/req.http +++ b/.csharp.source/req.http @@ -4,6 +4,7 @@ POST https://tokyohakuzencoltd-dev.outsystemsenterprise.com/BluememeCordovaCore/ Content-Type: application/json { + "TargetUserId": 23, "notification": { "title": "Push Test", "body": "This is a push test." diff --git a/README.md b/README.md index 686388d..85365b0 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,23 @@ BluememeUtils.getInitialNotification(success, error); BluememeUtils.stopNotificationListener(success, error); ``` -`getPushPermissionStatus` 只读取当前系统通知状态,不弹授权框。可用于填充后端 `NotificationsEnabled`。 +`getPushPermissionStatus` 只读取当前系统通知状态,不弹授权框。可用于填充后端 `NotificationsEnabled`。iOS 返回 `authorizationStatus: "notDetermined"` 时,说明还没有触发系统通知授权弹窗,需要先调用 `requestPushPermission`,用户授权后再调用 `getFirebaseToken`。 + +iOS 获取 FCM token 的推荐顺序: + +```js +BluememeUtils.requestPushPermission(function (permission) { + if (permission.enabled !== true) { + return; + } + + BluememeUtils.getFirebaseToken(function (result) { + console.log(result.token); + }, error); +}, error); +``` + +iOS 上 FCM token 依赖 APNs device token。插件会在 `getFirebaseToken` 内等待 Cordova 收到 APNs token 后再向 Firebase 请求 FCM token;如果 APNs token 超时或注册失败,会返回 `APNS_TOKEN_NOT_AVAILABLE` 或 `APNS_TOKEN_FAILED`。 返回示例: @@ -224,7 +240,10 @@ iOS WiFi: Firebase: - Android 13+ 需要通知权限,调用 `requestPushPermission`。 -- iOS 需要 Push Notifications capability、Background Modes 中的 Remote notifications,以及 APNs key/certificate 已配置到 Firebase。 +- iOS 需要 Apple Developer App ID 开启 Push Notifications,并使用重新生成的 provisioning profile 构建。 +- 插件会向 Cordova iOS entitlements 写入 `aps-environment`:Debug 为 `development`,Release 为 `production`。仍建议在构建后的 IPA 中检查签名 entitlements 是否包含该 key。 +- Firebase Console 需要在 Project Settings > Cloud Messaging > Apple app configuration 中上传 APNs Auth Key `.p8` 或 APNs certificate。推荐使用 APNs Auth Key。 +- Background Modes 中的 Remote notifications 只影响后台静默推送/后台唤醒,不是获取 APNs token 和 FCM token 的前置条件。 - 如果项目中还有其他推送插件,也可能同时修改 `UNUserNotificationCenterDelegate` 或 Firebase service,需要在真机上确认通知打开事件。 Biometric: @@ -249,6 +268,8 @@ Biometric: - `INVALID_WIFI_OPTIONS` - `WIFI_CONNECT_FAILED` - `FIREBASE_NOT_CONFIGURED` +- `APNS_TOKEN_NOT_AVAILABLE` +- `APNS_TOKEN_FAILED` - `FCM_TOKEN_FAILED` - `BIOMETRIC_UNAVAILABLE` - `AUTHENTICATION_ERROR` diff --git a/check-ios-hotspot-signature.sh b/check-ios-hotspot-signature.sh index fd62d02..2027391 100644 --- a/check-ios-hotspot-signature.sh +++ b/check-ios-hotspot-signature.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash set -u -ENTITLEMENT_KEY="com.apple.developer.networking.HotspotConfiguration" +HOTSPOT_ENTITLEMENT_KEY="com.apple.developer.networking.HotspotConfiguration" +APNS_ENTITLEMENT_KEY="aps-environment" +CHECK_SCRIPT_VERSION="2026-04-23.3" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TIMESTAMP="$(date '+%Y%m%d-%H%M%S')" @@ -23,8 +25,9 @@ Usage: $(basename "$0") /path/to/App.ipa Checks: - 1. embedded.mobileprovision contains ${ENTITLEMENT_KEY}=true - 2. App executable appears to contain ${ENTITLEMENT_KEY}=true + 1. embedded.mobileprovision contains ${HOTSPOT_ENTITLEMENT_KEY}=true + 2. embedded.mobileprovision contains ${APNS_ENTITLEMENT_KEY}=development|production + 3. App executable appears to contain both signed entitlements Notes: Linux cannot run Apple's codesign tool. The executable check is a best-effort @@ -59,22 +62,32 @@ has_command() { extract_plist_from_binary() { local source_file="$1" local target_file="$2" + local needle="${3:-}" - python3 - "$source_file" "$target_file" <<'PY' + python3 - "$source_file" "$target_file" "$needle" <<'PY' import sys from pathlib import Path source = Path(sys.argv[1]) target = Path(sys.argv[2]) +needle = sys.argv[3].encode("utf-8") if len(sys.argv) > 3 and sys.argv[3] else b"" data = source.read_bytes() -start = data.find(b"= 0: + start = data.rfind(b"", needle_index) +else: + start = data.find(b"", start if start >= 0 else 0) + if start < 0: sys.exit(2) -end = data.find(b"", start) if end < 0: sys.exit(3) @@ -97,7 +110,28 @@ key = sys.argv[2] data = path.read_bytes() try: - value = plistlib.loads(data).get(key) + plist = plistlib.loads(data) + + values = [] + + def collect(obj): + if isinstance(obj, dict): + if key in obj: + values.append(obj.get(key)) + entitlements = obj.get("Entitlements") + if isinstance(entitlements, dict) and key in entitlements: + values.append(entitlements.get(key)) + for value in obj.values(): + if isinstance(value, (dict, list, tuple)): + collect(value) + elif isinstance(obj, (list, tuple)): + for value in obj: + if isinstance(value, (dict, list, tuple)): + collect(value) + + collect(plist) + + value = values[0] if values else None if value is True: print("true") sys.exit(0) @@ -123,6 +157,63 @@ except Exception: PY } +check_plist_key_string_allowed() { + local plist_file="$1" + local key="$2" + shift 2 + + python3 - "$plist_file" "$key" "$@" <<'PY' +import plistlib +import re +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +key = sys.argv[2] +allowed = set(sys.argv[3:]) +data = path.read_bytes() + +def finish(value): + if value is None: + print("missing") + sys.exit(1) + print(value) + if str(value) in allowed: + sys.exit(0) + sys.exit(2) + +try: + plist = plistlib.loads(data) + values = [] + + def collect(obj): + if isinstance(obj, dict): + if key in obj: + values.append(obj.get(key)) + entitlements = obj.get("Entitlements") + if isinstance(entitlements, dict) and key in entitlements: + values.append(entitlements.get(key)) + for value in obj.values(): + if isinstance(value, (dict, list, tuple)): + collect(value) + elif isinstance(obj, (list, tuple)): + for value in obj: + if isinstance(value, (dict, list, tuple)): + collect(value) + + collect(plist) + finish(values[0] if values else None) +except Exception: + text = data.decode("utf-8", errors="ignore") + pattern = re.compile( + r"\s*" + re.escape(key) + r"\s*\s*\s*([^<]+?)\s*", + re.IGNORECASE, + ) + match = pattern.search(text) + finish(match.group(1).strip() if match else None) +PY +} + find_main_executable() { local app_dir="$1" local info_plist="${app_dir}/Info.plist" @@ -157,7 +248,8 @@ print_section() { echo "==== $* ====" } -echo "iOS Hotspot Configuration Signature Check" +echo "iOS Hotspot/APNs Signature Check" +echo "Checker version: ${CHECK_SCRIPT_VERSION}" echo "Started: $(date -Is)" echo "Script: ${BASH_SOURCE[0]}" echo "Log: ${LOG_FILE}" @@ -246,43 +338,71 @@ if [[ "${PROFILE_EXTRACTED}" -eq 0 ]]; then fi fi -PROFILE_RESULT="$(check_plist_key_true "${PROFILE_PLIST}" "${ENTITLEMENT_KEY}" 2>&1)" +PROFILE_RESULT="$(check_plist_key_true "${PROFILE_PLIST}" "${HOTSPOT_ENTITLEMENT_KEY}" 2>&1)" PROFILE_STATUS=$? -echo "Profile entitlement value: ${PROFILE_RESULT}" +echo "Profile Hotspot entitlement value: ${PROFILE_RESULT}" if [[ "${PROFILE_STATUS}" -eq 0 ]]; then - pass "Provisioning profile contains ${ENTITLEMENT_KEY}=true" + pass "Provisioning profile contains ${HOTSPOT_ENTITLEMENT_KEY}=true" else - fail "Provisioning profile does not contain ${ENTITLEMENT_KEY}=true" + fail "Provisioning profile does not contain ${HOTSPOT_ENTITLEMENT_KEY}=true" +fi + +PROFILE_APNS_RESULT="$(check_plist_key_string_allowed "${PROFILE_PLIST}" "${APNS_ENTITLEMENT_KEY}" development production 2>&1)" +PROFILE_APNS_STATUS=$? +echo "Profile APNs entitlement value: ${PROFILE_APNS_RESULT}" +if [[ "${PROFILE_APNS_STATUS}" -eq 0 ]]; then + pass "Provisioning profile contains ${APNS_ENTITLEMENT_KEY}=${PROFILE_APNS_RESULT}" +else + fail "Provisioning profile does not contain ${APNS_ENTITLEMENT_KEY}=development|production" fi echo echo "Profile entitlement context:" -grep -n -A2 -B1 "${ENTITLEMENT_KEY}" "${PROFILE_PLIST}" || true +grep -n -A2 -B1 "${HOTSPOT_ENTITLEMENT_KEY}" "${PROFILE_PLIST}" || true +grep -n -A2 -B1 "${APNS_ENTITLEMENT_KEY}" "${PROFILE_PLIST}" || true print_section "Signed App Entitlements" echo "Linux cannot run Apple's codesign. This is a best-effort scan of the app executable." -if grep -a -q "${ENTITLEMENT_KEY}" "${EXECUTABLE_PATH}"; then - pass "Executable contains entitlement key string: ${ENTITLEMENT_KEY}" +if grep -a -q "${HOTSPOT_ENTITLEMENT_KEY}" "${EXECUTABLE_PATH}"; then + pass "Executable contains entitlement key string: ${HOTSPOT_ENTITLEMENT_KEY}" else fail "Executable does not contain entitlement key string. Signed app entitlements likely do not include Hotspot Configuration." fi +if grep -a -q "${APNS_ENTITLEMENT_KEY}" "${EXECUTABLE_PATH}"; then + pass "Executable contains entitlement key string: ${APNS_ENTITLEMENT_KEY}" +else + fail "Executable does not contain entitlement key string. Signed app entitlements likely do not include APNs aps-environment." +fi + EXEC_ENTITLEMENTS_PLIST="${WORK_DIR}/signed-entitlements-heuristic.plist" -if extract_plist_from_binary "${EXECUTABLE_PATH}" "${EXEC_ENTITLEMENTS_PLIST}"; then +if extract_plist_from_binary "${EXECUTABLE_PATH}" "${EXEC_ENTITLEMENTS_PLIST}" "${HOTSPOT_ENTITLEMENT_KEY}"; then echo "Extracted executable plist candidate: ${EXEC_ENTITLEMENTS_PLIST}" - EXEC_RESULT="$(check_plist_key_true "${EXEC_ENTITLEMENTS_PLIST}" "${ENTITLEMENT_KEY}" 2>&1)" + EXEC_RESULT="$(check_plist_key_true "${EXEC_ENTITLEMENTS_PLIST}" "${HOTSPOT_ENTITLEMENT_KEY}" 2>&1)" EXEC_STATUS=$? - echo "Executable entitlement value: ${EXEC_RESULT}" + echo "Executable Hotspot entitlement value: ${EXEC_RESULT}" if [[ "${EXEC_STATUS}" -eq 0 ]]; then - pass "Executable plist candidate contains ${ENTITLEMENT_KEY}=true" + pass "Executable plist candidate contains ${HOTSPOT_ENTITLEMENT_KEY}=true" else warn "Executable contains entitlement key, but extracted plist candidate did not prove value=true." warn "Use macOS codesign for authoritative validation." fi + + EXEC_APNS_RESULT="$(check_plist_key_string_allowed "${EXEC_ENTITLEMENTS_PLIST}" "${APNS_ENTITLEMENT_KEY}" development production 2>&1)" + EXEC_APNS_STATUS=$? + echo "Executable APNs entitlement value: ${EXEC_APNS_RESULT}" + if [[ "${EXEC_APNS_STATUS}" -eq 0 ]]; then + pass "Executable plist candidate contains ${APNS_ENTITLEMENT_KEY}=${EXEC_APNS_RESULT}" + else + warn "Executable contains APNs entitlement key, but extracted plist candidate did not prove value=development|production." + warn "Use macOS codesign for authoritative validation." + fi + echo echo "Executable entitlement context:" - grep -n -A2 -B1 "${ENTITLEMENT_KEY}" "${EXEC_ENTITLEMENTS_PLIST}" || true + grep -n -A2 -B1 "${HOTSPOT_ENTITLEMENT_KEY}" "${EXEC_ENTITLEMENTS_PLIST}" || true + grep -n -A2 -B1 "${APNS_ENTITLEMENT_KEY}" "${EXEC_ENTITLEMENTS_PLIST}" || true else warn "Could not extract XML plist from executable. Key string was present, but value=true was not proven." warn "Use macOS codesign for authoritative validation." @@ -290,7 +410,9 @@ fi print_section "Summary" pass "Provisioning profile Hotspot entitlement: true" +pass "Provisioning profile APNs entitlement: ${PROFILE_APNS_RESULT}" pass "Executable Hotspot entitlement key: present" +pass "Executable APNs entitlement key: present" echo echo "Full log written to:" echo " ${LOG_FILE}" diff --git a/package.json b/package.json index 16678d9..2f39926 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bluememe-utils", - "version": "0.1.11", + "version": "0.1.12", "description": "Cordova utilities for WiFi, Firebase Cloud Messaging, and biometric unlock for OutSystems 11 apps.", "license": "MIT", "files": [ diff --git a/plugin.xml b/plugin.xml index fa290b8..5083ce0 100644 --- a/plugin.xml +++ b/plugin.xml @@ -2,7 +2,7 @@ + version="0.1.12"> bluememe-utils Cordova utilities for WiFi, Firebase Cloud Messaging, and biometric unlock. MIT @@ -104,6 +104,12 @@ + + development + + + production + diff --git a/src/ios/CDVBluememeUtils.m b/src/ios/CDVBluememeUtils.m index e460907..dbfdfde 100644 --- a/src/ios/CDVBluememeUtils.m +++ b/src/ios/CDVBluememeUtils.m @@ -8,12 +8,18 @@ #import static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_key"; -static NSString * const BluememeUtilsPluginVersion = @"0.1.11"; +static NSString * const BluememeUtilsPluginVersion = @"0.1.12"; +static NSString * const BluememeCordovaRemoteNotificationName = @"CDVRemoteNotification"; +static NSString * const BluememeCordovaRemoteNotificationErrorName = @"CDVRemoteNotificationError"; +static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0; @interface CDVBluememeUtils () @property (nonatomic, copy) NSString *notificationCallbackId; @property (nonatomic, strong) NSDictionary *initialNotification; @property (nonatomic, weak) id previousNotificationDelegate; +@property (nonatomic, strong) NSData *apnsDeviceToken; +@property (nonatomic, strong) NSError *apnsRegistrationError; +@property (nonatomic, strong) CDVInvokedUrlCommand *pendingFirebaseTokenCommand; @end @implementation CDVBluememeUtils @@ -29,6 +35,20 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11"; self.previousNotificationDelegate = center.delegate; center.delegate = self; } + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(remoteNotificationRegistered:) + name:BluememeCordovaRemoteNotificationName + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(remoteNotificationRegistrationFailed:) + name:BluememeCordovaRemoteNotificationErrorName + object:nil]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)saveWifi:(CDVInvokedUrlCommand *)command @@ -159,14 +179,31 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11"; return; } [[UIApplication sharedApplication] registerForRemoteNotifications]; - [self sendSuccessForCommand:command dictionary:@{@"type": @"notification", @"granted": @(granted), @"missing": granted ? @[] : @[@"notification"]}]; + [[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableDictionary *result = [[self dictionaryFromNotificationSettings:settings] mutableCopy]; + result[@"granted"] = @(granted); + result[@"registeredForRemoteNotifications"] = @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]); + result[@"apnsTokenAvailable"] = @(self.apnsDeviceToken.length > 0); + [self sendSuccessForCommand:command dictionary:result]; + }); + }]; }); }]; } else { UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound) categories:nil]; [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; [[UIApplication sharedApplication] registerForRemoteNotifications]; - [self sendSuccessForCommand:command dictionary:@{@"type": @"notification", @"granted": @YES, @"missing": @[]}]; + [self sendSuccessForCommand:command dictionary:@{ + @"type": @"notification", + @"platform": @"iOS", + @"enabled": @YES, + @"granted": @YES, + @"authorizationStatus": @"authorized", + @"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]), + @"apnsTokenAvailable": @(self.apnsDeviceToken.length > 0), + @"missing": @[] + }]; } } @@ -190,6 +227,8 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11"; @"alertEnabled": @((settings.types & UIUserNotificationTypeAlert) != 0), @"soundEnabled": @((settings.types & UIUserNotificationTypeSound) != 0), @"badgeEnabled": @((settings.types & UIUserNotificationTypeBadge) != 0), + @"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]), + @"apnsTokenAvailable": @(self.apnsDeviceToken.length > 0), @"missing": enabled ? @[] : @[@"notification"] }]; } @@ -204,13 +243,102 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11"; } [FIRMessaging messaging].delegate = self; + if (@available(iOS 10.0, *)) { + [[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (![self notificationAuthorizationAllowsFirebaseToken:settings.authorizationStatus]) { + NSMutableDictionary *details = [[self dictionaryFromNotificationSettings:settings] mutableCopy]; + details[@"apnsTokenAvailable"] = @(self.apnsDeviceToken.length > 0); + details[@"registeredForRemoteNotifications"] = @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]); + [self sendErrorForCommand:command + code:@"PERMISSION_REQUIRED" + message:@"Call requestPushPermission and wait for notification authorization before getFirebaseToken." + details:details]; + return; + } + [self registerForRemoteNotificationsAndFetchFirebaseTokenForCommand:command]; + }); + }]; + return; + } + + [self registerForRemoteNotificationsAndFetchFirebaseTokenForCommand:command]; +} + +- (void)registerForRemoteNotificationsAndFetchFirebaseTokenForCommand:(CDVInvokedUrlCommand *)command +{ + if (self.apnsDeviceToken.length > 0) { + [FIRMessaging messaging].APNSToken = self.apnsDeviceToken; + [self fetchFirebaseTokenForCommand:command]; + return; + } + + if (self.pendingFirebaseTokenCommand != nil) { + [self sendErrorForCommand:self.pendingFirebaseTokenCommand + code:@"FCM_TOKEN_REQUEST_REPLACED" + message:@"A newer getFirebaseToken request replaced this pending request." + details:@{ + @"apnsTokenAvailable": @NO, + @"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]) + }]; + } + + self.pendingFirebaseTokenCommand = command; + self.apnsRegistrationError = nil; + [[UIApplication sharedApplication] registerForRemoteNotifications]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(BluememeAPNSTokenTimeoutSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (self.pendingFirebaseTokenCommand != command) { + return; + } + + self.pendingFirebaseTokenCommand = nil; + if (self.apnsRegistrationError != nil) { + [self sendErrorForCommand:command + code:@"APNS_TOKEN_FAILED" + message:(self.apnsRegistrationError.localizedDescription ? self.apnsRegistrationError.localizedDescription : @"APNs registration failed.") + details:[self detailsForNativeError:self.apnsRegistrationError includeAPNSState:YES]]; + return; + } + + [self sendErrorForCommand:command + code:@"APNS_TOKEN_NOT_AVAILABLE" + message:@"APNs device token was not received before fetching the FCM token. Check Push Notifications capability, aps-environment entitlement, provisioning profile, and Firebase APNs key configuration." + details:@{ + @"apnsTokenAvailable": @NO, + @"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]), + @"timeoutSeconds": @(BluememeAPNSTokenTimeoutSeconds) + }]; + }); +} + +- (void)fetchFirebaseTokenForCommand:(CDVInvokedUrlCommand *)command +{ + if (self.apnsDeviceToken.length > 0) { + [FIRMessaging messaging].APNSToken = self.apnsDeviceToken; + } + [[FIRMessaging messaging] tokenWithCompletion:^(NSString *token, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ if (error != nil || token.length == 0) { - [self sendErrorForCommand:command code:@"FCM_TOKEN_FAILED" message:(error.localizedDescription ? error.localizedDescription : @"Failed to get Firebase token.")]; + NSMutableDictionary *details = [NSMutableDictionary dictionary]; + details[@"apnsTokenAvailable"] = @(self.apnsDeviceToken.length > 0); + details[@"registeredForRemoteNotifications"] = @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]); + if (error != nil) { + [details addEntriesFromDictionary:[self detailsForNativeError:error includeAPNSState:NO]]; + } + [self sendErrorForCommand:command + code:@"FCM_TOKEN_FAILED" + message:(error.localizedDescription ? error.localizedDescription : @"Failed to get Firebase token.") + details:details]; return; } - [self sendSuccessForCommand:command dictionary:@{@"token": token}]; + [self sendSuccessForCommand:command dictionary:@{ + @"token": token, + @"platform": @"iOS", + @"apnsTokenAvailable": @(self.apnsDeviceToken.length > 0), + @"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]) + }]; [self sendEvent:@{@"type": @"token", @"token": token}]; }); }]; @@ -716,6 +844,8 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11"; @"alertEnabled": @(alertEnabled), @"soundEnabled": @(soundEnabled), @"badgeEnabled": @(badgeEnabled), + @"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]), + @"apnsTokenAvailable": @(self.apnsDeviceToken.length > 0), @"missing": missing }; } @@ -746,6 +876,137 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11"; return @"unknown"; } +- (BOOL)notificationAuthorizationAllowsFirebaseToken:(UNAuthorizationStatus)status API_AVAILABLE(ios(10.0)) +{ + if (status == UNAuthorizationStatusAuthorized) { + return YES; + } + if (@available(iOS 12.0, *)) { + if (status == UNAuthorizationStatusProvisional) { + return YES; + } + } + if (@available(iOS 14.0, *)) { + if (status == UNAuthorizationStatusEphemeral) { + return YES; + } + } + return NO; +} + +- (void)remoteNotificationRegistered:(NSNotification *)notification +{ + NSData *tokenData = [self dataFromAPNSTokenObject:notification.object]; + if (tokenData.length == 0) { + tokenData = [self dataFromAPNSTokenObject:notification.userInfo[@"deviceToken"]]; + } + if (tokenData.length == 0) { + return; + } + + self.apnsDeviceToken = tokenData; + self.apnsRegistrationError = nil; + if ([FIRApp defaultApp] != nil) { + [FIRMessaging messaging].APNSToken = tokenData; + } + + if (self.pendingFirebaseTokenCommand != nil) { + CDVInvokedUrlCommand *command = self.pendingFirebaseTokenCommand; + self.pendingFirebaseTokenCommand = nil; + [self fetchFirebaseTokenForCommand:command]; + } +} + +- (void)remoteNotificationRegistrationFailed:(NSNotification *)notification +{ + NSError *error = nil; + if ([notification.object isKindOfClass:[NSError class]]) { + error = notification.object; + } else if ([notification.userInfo[@"error"] isKindOfClass:[NSError class]]) { + error = notification.userInfo[@"error"]; + } + + self.apnsRegistrationError = error; + + if (self.pendingFirebaseTokenCommand != nil) { + CDVInvokedUrlCommand *command = self.pendingFirebaseTokenCommand; + self.pendingFirebaseTokenCommand = nil; + [self sendErrorForCommand:command + code:@"APNS_TOKEN_FAILED" + message:(error.localizedDescription ? error.localizedDescription : @"APNs registration failed.") + details:[self detailsForNativeError:error includeAPNSState:YES]]; + } +} + +- (NSData *)dataFromAPNSTokenObject:(id)object +{ + if ([object isKindOfClass:[NSData class]]) { + return object; + } + if (![object isKindOfClass:[NSString class]]) { + return nil; + } + + NSString *tokenString = (NSString *)object; + NSRange angleStart = [tokenString rangeOfString:@"<"]; + NSRange angleEnd = [tokenString rangeOfString:@">" options:NSBackwardsSearch]; + if (angleStart.location != NSNotFound && angleEnd.location != NSNotFound && angleEnd.location > angleStart.location) { + tokenString = [tokenString substringWithRange:NSMakeRange(angleStart.location + 1, angleEnd.location - angleStart.location - 1)]; + } else { + NSRange bytesRange = [tokenString rangeOfString:@"bytes = 0x"]; + if (bytesRange.location != NSNotFound) { + tokenString = [tokenString substringFromIndex:bytesRange.location + bytesRange.length]; + } + } + + NSMutableString *hex = [NSMutableString string]; + NSCharacterSet *hexCharacters = [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdefABCDEF"]; + for (NSUInteger index = 0; index < tokenString.length; index++) { + unichar character = [tokenString characterAtIndex:index]; + if ([hexCharacters characterIsMember:character]) { + [hex appendFormat:@"%C", character]; + } else if ([tokenString rangeOfString:@"bytes = 0x"].location != NSNotFound) { + break; + } + } + + if (hex.length == 0 || (hex.length % 2) != 0) { + return nil; + } + + NSMutableData *data = [NSMutableData dataWithCapacity:(hex.length / 2)]; + for (NSUInteger index = 0; index < hex.length; index += 2) { + NSString *byteString = [hex substringWithRange:NSMakeRange(index, 2)]; + unsigned int byte = 0; + NSScanner *scanner = [NSScanner scannerWithString:byteString]; + if (![scanner scanHexInt:&byte]) { + return nil; + } + unsigned char value = (unsigned char)byte; + [data appendBytes:&value length:1]; + } + return data; +} + +- (NSDictionary *)detailsForNativeError:(NSError *)error includeAPNSState:(BOOL)includeAPNSState +{ + NSMutableDictionary *details = [NSMutableDictionary dictionary]; + if (includeAPNSState) { + details[@"apnsTokenAvailable"] = @(self.apnsDeviceToken.length > 0); + details[@"registeredForRemoteNotifications"] = @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]); + } + if (error.domain.length > 0) { + details[@"nativeDomain"] = error.domain; + } + if (error != nil) { + details[@"nativeCode"] = @(error.code); + } + if (error.localizedDescription.length > 0) { + details[@"nativeError"] = error.localizedDescription; + } + return details; +} + - (BOOL)ensureFirebaseConfigured:(NSError **)error { if ([FIRApp defaultApp] != nil) {