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) {