•插件初始化时会给宿主 AppDelegate 安装 APNs 注册回调桥接。

•捕获:
◦application:didRegisterForRemoteNotificationsWithDeviceToken:
◦application:didFailToRegisterForRemoteNotificationsWithError:
•即使宿主没有转发 Cordova 的 CDVRemoteNotification,插件也能自己收到 APNs token。
•getFirebaseToken 等待 APNs token 时,会同时轮询 Firebase Messaging 已缓存的 APNSToken。
•返回结果增加诊断字段:
◦apnsTokenSource: plugin / firebase / none
◦cachedAPNSTokenAvailable
◦firebaseAPNSTokenAvailable
This commit is contained in:
aria committed 2026-05-25 11:26:31 +09:00
1 parent 63703b966e
commit 8fcb440d8a
4 files changed
+221 -54

No files matched your search

+9 -1
View File
@@ -152,7 +152,15 @@ BluememeUtils.requestPushPermission(function (permission) {
}, error);
```
iOS 上 FCM token 依赖 APNs device token。插件会在 `getFirebaseToken` 内等待 Cordova 收到 APNs token 后再向 Firebase 请求 FCM token;如果 APNs token 超时或注册失败,会返回 `APNS_TOKEN_NOT_AVAILABLE` 或 `APNS_TOKEN_FAILED`。
iOS 上 FCM token 依赖 APNs device token。必须等待 `requestPushPermission` 的 success callback 返回后再调用 `getFirebaseToken`,不要把 `requestPushPermission()` 当成同步调用。
插件会在 `getFirebaseToken` 内等待 APNs token 后再向 Firebase 请求 FCM token。iOS 侧会同时使用三种来源降低宿主 AppDelegate 未转发回调造成的问题:
- 直接桥接 `application:didRegisterForRemoteNotificationsWithDeviceToken:`
- 监听 Cordova 的 `CDVRemoteNotification`
- 轮询 Firebase Messaging 已缓存的 `APNSToken`
如果 APNs token 超时或注册失败,会返回 `APNS_TOKEN_NOT_AVAILABLE` 或 `APNS_TOKEN_FAILED`。返回结果会包含 `apnsTokenSource`、`cachedAPNSTokenAvailable`、`firebaseAPNSTokenAvailable` 等诊断字段。
返回示例:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bluememe-utils",
"version": "0.1.12",
"version": "0.1.13",
"description": "Cordova utilities for WiFi, Firebase Cloud Messaging, and biometric unlock for OutSystems 11 apps.",
"license": "MIT",
"files": [
+1 -1
View File
@@ -2,7 +2,7 @@
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android"
id="bluememe-utils"
version="0.1.12">
version="0.1.13">
<name>bluememe-utils</name>
<description>Cordova utilities for WiFi, Firebase Cloud Messaging, and biometric unlock.</description>
<license>MIT</license>
+210 -51
View File
@@ -6,12 +6,61 @@
#import <SystemConfiguration/CaptiveNetwork.h>
#import <TargetConditionals.h>
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_key";
static NSString * const BluememeUtilsPluginVersion = @"0.1.12";
static NSString * const BluememeUtilsPluginVersion = @"0.1.13";
static NSString * const BluememeCordovaRemoteNotificationName = @"CDVRemoteNotification";
static NSString * const BluememeCordovaRemoteNotificationErrorName = @"CDVRemoteNotificationError";
static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
static NSTimeInterval const BluememeAPNSTokenPollIntervalSeconds = 0.25;
static char BluememeRemoteNotificationBridgeInstalledKey;
static SEL BluememeRemoteNotificationBridgeSelectorForSelector(SEL selector)
{
NSString *selectorName = NSStringFromSelector(selector);
return NSSelectorFromString([@"bluememe_" stringByAppendingString:selectorName]);
}
static void BluememePostRemoteNotificationToken(NSData *deviceToken)
{
if (deviceToken.length == 0) {
return;
}
[[NSNotificationCenter defaultCenter] postNotificationName:BluememeCordovaRemoteNotificationName
object:deviceToken
userInfo:@{@"deviceToken": deviceToken}];
}
static void BluememePostRemoteNotificationError(NSError *error)
{
NSDictionary *userInfo = error == nil ? @{} : @{@"error": error};
[[NSNotificationCenter defaultCenter] postNotificationName:BluememeCordovaRemoteNotificationErrorName
object:error
userInfo:userInfo];
}
static void BluememeApplicationDidRegisterForRemoteNotifications(id self, SEL _cmd, UIApplication *application, NSData *deviceToken)
{
BluememePostRemoteNotificationToken(deviceToken);
SEL originalSelector = BluememeRemoteNotificationBridgeSelectorForSelector(_cmd);
if ([self respondsToSelector:originalSelector]) {
IMP originalImplementation = [self methodForSelector:originalSelector];
((void (*)(id, SEL, UIApplication *, NSData *))originalImplementation)(self, originalSelector, application, deviceToken);
}
}
static void BluememeApplicationDidFailToRegisterForRemoteNotifications(id self, SEL _cmd, UIApplication *application, NSError *error)
{
BluememePostRemoteNotificationError(error);
SEL originalSelector = BluememeRemoteNotificationBridgeSelectorForSelector(_cmd);
if ([self respondsToSelector:originalSelector]) {
IMP originalImplementation = [self methodForSelector:originalSelector];
((void (*)(id, SEL, UIApplication *, NSError *))originalImplementation)(self, originalSelector, application, error);
}
}
@interface CDVBluememeUtils ()
@property (nonatomic, copy) NSString *notificationCallbackId;
@@ -29,6 +78,7 @@ static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
if ([self ensureFirebaseConfigured:nil]) {
[FIRMessaging messaging].delegate = self;
}
[self installRemoteNotificationDelegateBridgeIfNeeded];
if (@available(iOS 10.0, *)) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
@@ -183,8 +233,7 @@ static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
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);
[result addEntriesFromDictionary:[self apnsStateDetails]];
[self sendSuccessForCommand:command dictionary:result];
});
}];
@@ -194,16 +243,16 @@ static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound) categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
[[UIApplication sharedApplication] registerForRemoteNotifications];
[self sendSuccessForCommand:command dictionary:@{
NSMutableDictionary *result = [[self apnsStateDetails] mutableCopy];
[result addEntriesFromDictionary:@{
@"type": @"notification",
@"platform": @"iOS",
@"enabled": @YES,
@"granted": @YES,
@"authorizationStatus": @"authorized",
@"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]),
@"apnsTokenAvailable": @(self.apnsDeviceToken.length > 0),
@"missing": @[]
}];
[self sendSuccessForCommand:command dictionary:result];
}
}
@@ -218,7 +267,8 @@ static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
} else {
UIUserNotificationSettings *settings = [[UIApplication sharedApplication] currentUserNotificationSettings];
BOOL enabled = settings.types != UIUserNotificationTypeNone;
[self sendSuccessForCommand:command dictionary:@{
NSMutableDictionary *result = [[self apnsStateDetails] mutableCopy];
[result addEntriesFromDictionary:@{
@"type": @"notification",
@"platform": @"iOS",
@"enabled": @(enabled),
@@ -227,10 +277,9 @@ static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
@"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"]
}];
[self sendSuccessForCommand:command dictionary:result];
}
}
@@ -248,8 +297,7 @@ static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
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]);
[details addEntriesFromDictionary:[self apnsStateDetails]];
[self sendErrorForCommand:command
code:@"PERMISSION_REQUIRED"
message:@"Call requestPushPermission and wait for notification authorization before getFirebaseToken."
@@ -267,63 +315,80 @@ static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
- (void)registerForRemoteNotificationsAndFetchFirebaseTokenForCommand:(CDVInvokedUrlCommand *)command
{
if (self.apnsDeviceToken.length > 0) {
[FIRMessaging messaging].APNSToken = self.apnsDeviceToken;
NSData *tokenData = [self availableAPNSToken];
if (tokenData.length > 0) {
[FIRMessaging messaging].APNSToken = tokenData;
[self fetchFirebaseTokenForCommand:command];
return;
}
if (self.pendingFirebaseTokenCommand != nil) {
NSMutableDictionary *details = [[self apnsStateDetails] mutableCopy];
details[@"apnsTokenAvailable"] = @NO;
[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])
}];
details:details];
}
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;
}
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:BluememeAPNSTokenTimeoutSeconds];
[self waitForAPNSTokenAndFetchFirebaseTokenForCommand:command deadline:deadline];
}
- (void)waitForAPNSTokenAndFetchFirebaseTokenForCommand:(CDVInvokedUrlCommand *)command deadline:(NSDate *)deadline
{
if (self.pendingFirebaseTokenCommand != command) {
return;
}
NSData *tokenData = [self availableAPNSToken];
if (tokenData.length > 0) {
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;
}
[FIRMessaging messaging].APNSToken = tokenData;
[self fetchFirebaseTokenForCommand:command];
return;
}
if (self.apnsRegistrationError != nil) {
self.pendingFirebaseTokenCommand = 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;
}
if ([deadline timeIntervalSinceNow] <= 0) {
self.pendingFirebaseTokenCommand = nil;
NSMutableDictionary *details = [[self apnsStateDetails] mutableCopy];
details[@"timeoutSeconds"] = @(BluememeAPNSTokenTimeoutSeconds);
[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)
}];
message:@"APNs device token was not received before fetching the FCM token. Check Push Notifications capability, aps-environment entitlement, provisioning profile, Firebase APNs key configuration, or AppDelegate remote notification forwarding."
details:details];
return;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(BluememeAPNSTokenPollIntervalSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self waitForAPNSTokenAndFetchFirebaseTokenForCommand:command deadline:deadline];
});
}
- (void)fetchFirebaseTokenForCommand:(CDVInvokedUrlCommand *)command
{
if (self.apnsDeviceToken.length > 0) {
[FIRMessaging messaging].APNSToken = self.apnsDeviceToken;
NSData *tokenData = [self availableAPNSToken];
if (tokenData.length > 0) {
[FIRMessaging messaging].APNSToken = tokenData;
}
[[FIRMessaging messaging] tokenWithCompletion:^(NSString *token, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error != nil || token.length == 0) {
NSMutableDictionary *details = [NSMutableDictionary dictionary];
details[@"apnsTokenAvailable"] = @(self.apnsDeviceToken.length > 0);
details[@"registeredForRemoteNotifications"] = @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]);
NSMutableDictionary *details = [[self apnsStateDetails] mutableCopy];
if (error != nil) {
[details addEntriesFromDictionary:[self detailsForNativeError:error includeAPNSState:NO]];
}
@@ -333,12 +398,10 @@ static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
details:details];
return;
}
[self sendSuccessForCommand:command dictionary:@{
@"token": token,
@"platform": @"iOS",
@"apnsTokenAvailable": @(self.apnsDeviceToken.length > 0),
@"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications])
}];
NSMutableDictionary *result = [[self apnsStateDetails] mutableCopy];
result[@"token"] = token;
result[@"platform"] = @"iOS";
[self sendSuccessForCommand:command dictionary:result];
[self sendEvent:@{@"type": @"token", @"token": token}];
});
}];
@@ -835,7 +898,8 @@ static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
BOOL badgeEnabled = settings.badgeSetting == UNNotificationSettingEnabled;
NSArray *missing = enabled ? @[] : @[@"notification"];
return @{
NSMutableDictionary *result = [[self apnsStateDetails] mutableCopy];
[result addEntriesFromDictionary:@{
@"type": @"notification",
@"platform": @"iOS",
@"enabled": @(enabled),
@@ -844,10 +908,9 @@ static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
@"alertEnabled": @(alertEnabled),
@"soundEnabled": @(soundEnabled),
@"badgeEnabled": @(badgeEnabled),
@"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]),
@"apnsTokenAvailable": @(self.apnsDeviceToken.length > 0),
@"missing": missing
};
}];
return result;
}
- (NSString *)authorizationStatusName:(UNAuthorizationStatus)status API_AVAILABLE(ios(10.0))
@@ -894,6 +957,103 @@ static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
return NO;
}
- (NSData *)firebaseAPNSToken
{
if ([FIRApp defaultApp] == nil) {
return nil;
}
return [FIRMessaging messaging].APNSToken;
}
- (NSData *)availableAPNSToken
{
if (self.apnsDeviceToken.length > 0) {
return self.apnsDeviceToken;
}
NSData *firebaseToken = [self firebaseAPNSToken];
if (firebaseToken.length > 0) {
self.apnsDeviceToken = firebaseToken;
self.apnsRegistrationError = nil;
return firebaseToken;
}
return nil;
}
- (NSDictionary *)apnsStateDetails
{
NSData *cachedToken = self.apnsDeviceToken;
NSData *firebaseToken = [self firebaseAPNSToken];
BOOL cachedTokenAvailable = cachedToken.length > 0;
BOOL firebaseTokenAvailable = firebaseToken.length > 0;
NSString *source = @"none";
if (cachedTokenAvailable) {
source = @"plugin";
} else if (firebaseTokenAvailable) {
source = @"firebase";
}
return @{
@"apnsTokenAvailable": @(cachedTokenAvailable || firebaseTokenAvailable),
@"cachedAPNSTokenAvailable": @(cachedTokenAvailable),
@"firebaseAPNSTokenAvailable": @(firebaseTokenAvailable),
@"apnsTokenSource": source,
@"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications])
};
}
- (void)installRemoteNotificationDelegateBridgeIfNeeded
{
id<UIApplicationDelegate> appDelegate = [UIApplication sharedApplication].delegate;
if (appDelegate == nil) {
return;
}
Class appDelegateClass = [appDelegate class];
if ([objc_getAssociatedObject(appDelegateClass, &BluememeRemoteNotificationBridgeInstalledKey) boolValue]) {
return;
}
[self installRemoteNotificationBridgeForClass:appDelegateClass
selector:@selector(application:didRegisterForRemoteNotificationsWithDeviceToken:)
implementation:(IMP)BluememeApplicationDidRegisterForRemoteNotifications
typeEncoding:"v@:@@"];
[self installRemoteNotificationBridgeForClass:appDelegateClass
selector:@selector(application:didFailToRegisterForRemoteNotificationsWithError:)
implementation:(IMP)BluememeApplicationDidFailToRegisterForRemoteNotifications
typeEncoding:"v@:@@"];
objc_setAssociatedObject(appDelegateClass, &BluememeRemoteNotificationBridgeInstalledKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)installRemoteNotificationBridgeForClass:(Class)delegateClass
selector:(SEL)selector
implementation:(IMP)implementation
typeEncoding:(const char *)fallbackTypeEncoding
{
Method originalMethod = class_getInstanceMethod(delegateClass, selector);
const char *originalTypeEncoding = originalMethod == NULL ? NULL : method_getTypeEncoding(originalMethod);
const char *typeEncoding = originalTypeEncoding == NULL ? fallbackTypeEncoding : originalTypeEncoding;
if (originalMethod == NULL) {
class_addMethod(delegateClass, selector, implementation, typeEncoding);
return;
}
SEL bridgeSelector = BluememeRemoteNotificationBridgeSelectorForSelector(selector);
if (class_addMethod(delegateClass, selector, implementation, typeEncoding)) {
class_addMethod(delegateClass, bridgeSelector, method_getImplementation(originalMethod), typeEncoding);
return;
}
if (!class_addMethod(delegateClass, bridgeSelector, implementation, typeEncoding)) {
return;
}
Method bridgeMethod = class_getInstanceMethod(delegateClass, bridgeSelector);
originalMethod = class_getInstanceMethod(delegateClass, selector);
method_exchangeImplementations(originalMethod, bridgeMethod);
}
- (void)remoteNotificationRegistered:(NSNotification *)notification
{
NSData *tokenData = [self dataFromAPNSTokenObject:notification.object];
@@ -992,8 +1152,7 @@ static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
{
NSMutableDictionary *details = [NSMutableDictionary dictionary];
if (includeAPNSState) {
details[@"apnsTokenAvailable"] = @(self.apnsDeviceToken.length > 0);
details[@"registeredForRemoteNotifications"] = @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]);
[details addEntriesFromDictionary:[self apnsStateDetails]];
}
if (error.domain.length > 0) {
details[@"nativeDomain"] = error.domain;