first commit

This commit is contained in:
aria committed 2026-04-17 10:36:57 +09:00
commit 4140922530
10 files changed
+2362

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
#import <Cordova/CDV.h>
#import <FirebaseMessaging/FirebaseMessaging.h>
#import <UserNotifications/UserNotifications.h>
@interface CDVBluememeUtils : CDVPlugin <FIRMessagingDelegate, UNUserNotificationCenterDelegate>
@end
+490
View File
@@ -0,0 +1,490 @@
#import "CDVBluememeUtils.h"
#import <FirebaseCore/FirebaseCore.h>
#import <LocalAuthentication/LocalAuthentication.h>
#import <NetworkExtension/NetworkExtension.h>
#import <UIKit/UIKit.h>
@interface CDVBluememeUtils ()
@property (nonatomic, copy) NSString *notificationCallbackId;
@property (nonatomic, strong) NSDictionary *initialNotification;
@property (nonatomic, weak) id<UNUserNotificationCenterDelegate> previousNotificationDelegate;
@end
@implementation CDVBluememeUtils
- (void)pluginInitialize
{
if ([self ensureFirebaseConfigured:nil]) {
[FIRMessaging messaging].delegate = self;
}
if (@available(iOS 10.0, *)) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
self.previousNotificationDelegate = center.delegate;
center.delegate = self;
}
}
- (void)saveWifi:(CDVInvokedUrlCommand *)command
{
[self applyWifiCommand:command defaultJoinOnce:NO];
}
- (void)connectWifi:(CDVInvokedUrlCommand *)command
{
[self applyWifiCommand:command defaultJoinOnce:YES];
}
- (void)removeWifi:(CDVInvokedUrlCommand *)command
{
NSDictionary *options = [self firstDictionaryArgument:command];
NSString *ssid = [self stringFromDictionary:options key:@"ssid"];
if (ssid.length == 0) {
[self sendErrorForCommand:command code:@"INVALID_WIFI_OPTIONS" message:@"ssid is required."];
return;
}
if (@available(iOS 11.0, *)) {
[[NEHotspotConfigurationManager sharedManager] removeConfigurationForSSID:ssid];
[self sendSuccessForCommand:command dictionary:@{@"ssid": ssid, @"removed": @YES}];
} else {
[self sendErrorForCommand:command code:@"UNSUPPORTED_IOS_VERSION" message:@"WiFi configuration requires iOS 11 or newer."];
}
}
- (void)disconnectWifi:(CDVInvokedUrlCommand *)command
{
[self sendErrorForCommand:command code:@"UNSUPPORTED_PLATFORM_ACTION" message:@"iOS does not expose a public API for disconnecting WiFi from third-party apps."];
}
- (void)requestWifiPermission:(CDVInvokedUrlCommand *)command
{
[self sendSuccessForCommand:command dictionary:@{@"type": @"wifi", @"granted": @YES, @"missing": @[]}];
}
- (void)isBiometricAvailable:(CDVInvokedUrlCommand *)command
{
[self sendSuccessForCommand:command dictionary:[self biometricAvailability]];
}
- (void)unlockWithBiometrics:(CDVInvokedUrlCommand *)command
{
NSDictionary *options = [self firstDictionaryArgument:command];
LAContext *context = [[LAContext alloc] init];
NSString *fallbackTitle = [self stringFromDictionary:options key:@"fallbackTitle"];
NSString *cancelTitle = [self stringFromDictionary:options key:@"cancelTitle"];
if (fallbackTitle.length > 0) {
context.localizedFallbackTitle = fallbackTitle;
}
if (cancelTitle.length > 0) {
context.localizedCancelTitle = cancelTitle;
}
BOOL allowPasscode = [self boolFromDictionary:options key:@"allowDeviceCredential" defaultValue:YES];
LAPolicy policy = allowPasscode ? LAPolicyDeviceOwnerAuthentication : LAPolicyDeviceOwnerAuthenticationWithBiometrics;
NSError *availabilityError = nil;
if (![context canEvaluatePolicy:policy error:&availabilityError]) {
[self sendErrorForCommand:command code:@"BIOMETRIC_UNAVAILABLE" message:(availabilityError.localizedDescription ? availabilityError.localizedDescription : @"Biometric authentication is unavailable.")];
return;
}
NSString *reason = [self stringFromDictionary:options key:@"reason"];
if (reason.length == 0) {
reason = @"Unlock";
}
[context evaluatePolicy:policy localizedReason:reason reply:^(BOOL success, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (success) {
NSDictionary *result = @{@"unlocked": @YES, @"biometryType": [self biometryTypeForContext:context]};
[self sendSuccessForCommand:command dictionary:result];
NSMutableDictionary *event = [result mutableCopy];
event[@"type"] = @"unlock";
[self sendEvent:event];
} else {
[self sendErrorForCommand:command code:@"AUTHENTICATION_ERROR" message:(error.localizedDescription ? error.localizedDescription : @"Authentication failed.")];
}
});
}];
}
- (void)requestPushPermission:(CDVInvokedUrlCommand *)command
{
NSError *firebaseError = nil;
if (![self ensureFirebaseConfigured:&firebaseError]) {
[self sendErrorForCommand:command code:@"FIREBASE_NOT_CONFIGURED" message:(firebaseError.localizedDescription ? firebaseError.localizedDescription : @"Firebase is not configured.")];
return;
}
[FIRMessaging messaging].delegate = self;
if (@available(iOS 10.0, *)) {
UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound;
[[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error != nil) {
[self sendErrorForCommand:command code:@"PUSH_PERMISSION_FAILED" message:error.localizedDescription];
return;
}
[[UIApplication sharedApplication] registerForRemoteNotifications];
[self sendSuccessForCommand:command dictionary:@{@"type": @"notification", @"granted": @(granted), @"missing": granted ? @[] : @[@"notification"]}];
});
}];
} 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": @[]}];
}
}
- (void)getFirebaseToken:(CDVInvokedUrlCommand *)command
{
NSError *firebaseError = nil;
if (![self ensureFirebaseConfigured:&firebaseError]) {
[self sendErrorForCommand:command code:@"FIREBASE_NOT_CONFIGURED" message:(firebaseError.localizedDescription ? firebaseError.localizedDescription : @"Firebase is not configured.")];
return;
}
[FIRMessaging messaging].delegate = self;
[[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.")];
return;
}
[self sendSuccessForCommand:command dictionary:@{@"token": token}];
[self sendEvent:@{@"type": @"token", @"token": token}];
});
}];
}
- (void)subscribeToTopic:(CDVInvokedUrlCommand *)command
{
NSString *topic = [command.arguments firstObject];
if (![topic isKindOfClass:[NSString class]] || topic.length == 0) {
[self sendErrorForCommand:command code:@"INVALID_TOPIC" message:@"topic is required."];
return;
}
NSError *firebaseError = nil;
if (![self ensureFirebaseConfigured:&firebaseError]) {
[self sendErrorForCommand:command code:@"FIREBASE_NOT_CONFIGURED" message:(firebaseError.localizedDescription ? firebaseError.localizedDescription : @"Firebase is not configured.")];
return;
}
[[FIRMessaging messaging] subscribeToTopic:topic completion:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error != nil) {
[self sendErrorForCommand:command code:@"FIREBASE_TASK_FAILED" message:error.localizedDescription];
} else {
[self sendSuccessForCommand:command dictionary:nil];
}
});
}];
}
- (void)unsubscribeFromTopic:(CDVInvokedUrlCommand *)command
{
NSString *topic = [command.arguments firstObject];
if (![topic isKindOfClass:[NSString class]] || topic.length == 0) {
[self sendErrorForCommand:command code:@"INVALID_TOPIC" message:@"topic is required."];
return;
}
NSError *firebaseError = nil;
if (![self ensureFirebaseConfigured:&firebaseError]) {
[self sendErrorForCommand:command code:@"FIREBASE_NOT_CONFIGURED" message:(firebaseError.localizedDescription ? firebaseError.localizedDescription : @"Firebase is not configured.")];
return;
}
[[FIRMessaging messaging] unsubscribeFromTopic:topic completion:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error != nil) {
[self sendErrorForCommand:command code:@"FIREBASE_TASK_FAILED" message:error.localizedDescription];
} else {
[self sendSuccessForCommand:command dictionary:nil];
}
});
}];
}
- (void)getInitialNotification:(CDVInvokedUrlCommand *)command
{
[self sendSuccessForCommand:command dictionary:self.initialNotification];
self.initialNotification = nil;
}
- (void)startNotificationListener:(CDVInvokedUrlCommand *)command
{
self.notificationCallbackId = command.callbackId;
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT];
[result setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
- (void)stopNotificationListener:(CDVInvokedUrlCommand *)command
{
self.notificationCallbackId = nil;
[self sendSuccessForCommand:command dictionary:nil];
}
- (void)applyWifiCommand:(CDVInvokedUrlCommand *)command defaultJoinOnce:(BOOL)defaultJoinOnce
{
NSDictionary *options = [self firstDictionaryArgument:command];
NSString *ssid = [self stringFromDictionary:options key:@"ssid"];
NSString *password = [self stringFromDictionary:options key:@"password"];
NSString *security = [[self stringFromDictionary:options key:@"security"] lowercaseString];
BOOL isWep = [security isEqualToString:@"wep"] || [self boolFromDictionary:options key:@"isWep" defaultValue:NO];
BOOL joinOnce = [self boolFromDictionary:options key:@"joinOnce" defaultValue:defaultJoinOnce];
if (ssid.length == 0) {
[self sendErrorForCommand:command code:@"INVALID_WIFI_OPTIONS" message:@"ssid is required."];
return;
}
if (@available(iOS 11.0, *)) {
NEHotspotConfiguration *configuration = nil;
if (password.length == 0 || [security isEqualToString:@"open"]) {
configuration = [[NEHotspotConfiguration alloc] initWithSSID:ssid];
} else {
configuration = [[NEHotspotConfiguration alloc] initWithSSID:ssid passphrase:password isWEP:isWep];
}
configuration.joinOnce = joinOnce;
[[NEHotspotConfigurationManager sharedManager] applyConfiguration:configuration completionHandler:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error != nil && error.code != NEHotspotConfigurationErrorAlreadyAssociated) {
[self sendErrorForCommand:command code:@"WIFI_CONNECT_FAILED" message:(error.localizedDescription ? error.localizedDescription : @"Unable to apply WiFi configuration.")];
return;
}
NSDictionary *result = @{
@"ssid": ssid,
@"platformMode": @"hotspotConfiguration",
@"connected": @YES,
@"saved": @(!joinOnce),
@"alreadyAssociated": @(error != nil && error.code == NEHotspotConfigurationErrorAlreadyAssociated)
};
[self sendSuccessForCommand:command dictionary:result];
});
}];
} else {
[self sendErrorForCommand:command code:@"UNSUPPORTED_IOS_VERSION" message:@"WiFi configuration requires iOS 11 or newer."];
}
}
- (NSDictionary *)biometricAvailability
{
LAContext *context = [[LAContext alloc] init];
NSError *error = nil;
BOOL available = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error];
NSString *type = [self biometryTypeForContext:context];
if (available) {
return @{@"available": @YES, @"biometryType": type, @"code": @"OK", @"message": @"Biometric authentication is available."};
}
BOOL passcodeAvailable = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:nil];
return @{
@"available": @NO,
@"biometryType": type,
@"passcodeAvailable": @(passcodeAvailable),
@"code": @"BIOMETRIC_UNAVAILABLE",
@"message": error.localizedDescription ? error.localizedDescription : @"Biometric authentication is unavailable."
};
}
- (NSString *)biometryTypeForContext:(LAContext *)context
{
if (@available(iOS 11.0, *)) {
if (context.biometryType == LABiometryTypeFaceID) {
return @"face";
}
if (context.biometryType == LABiometryTypeTouchID) {
return @"touch";
}
if (@available(iOS 11.2, *)) {
if (context.biometryType == LABiometryTypeNone) {
return @"none";
}
}
return @"biometric";
}
return @"touch";
}
- (BOOL)ensureFirebaseConfigured:(NSError **)error
{
if ([FIRApp defaultApp] != nil) {
return YES;
}
NSString *plistPath = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"];
if (plistPath.length > 0) {
[FIRApp configure];
return [FIRApp defaultApp] != nil;
}
NSString *googleAppId = [self infoStringForKey:@"BLUEMEME_FIREBASE_GOOGLE_APP_ID"];
NSString *gcmSenderId = [self infoStringForKey:@"BLUEMEME_FIREBASE_GCM_SENDER_ID"];
NSString *apiKey = [self infoStringForKey:@"BLUEMEME_FIREBASE_API_KEY"];
NSString *projectId = [self infoStringForKey:@"BLUEMEME_FIREBASE_PROJECT_ID"];
NSString *storageBucket = [self infoStringForKey:@"BLUEMEME_FIREBASE_STORAGE_BUCKET"];
if (googleAppId.length == 0 || gcmSenderId.length == 0) {
if (error != nil) {
*error = [NSError errorWithDomain:@"BluememeUtils" code:1001 userInfo:@{NSLocalizedDescriptionKey: @"Firebase options are missing."}];
}
return NO;
}
FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:googleAppId GCMSenderID:gcmSenderId];
if (apiKey.length > 0) {
options.APIKey = apiKey;
}
if (projectId.length > 0) {
options.projectID = projectId;
}
if (storageBucket.length > 0) {
options.storageBucket = storageBucket;
}
options.bundleID = [[NSBundle mainBundle] bundleIdentifier];
[FIRApp configureWithOptions:options];
return [FIRApp defaultApp] != nil;
}
- (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken
{
if (fcmToken.length == 0) {
return;
}
[self sendEvent:@{@"type": @"token", @"token": fcmToken}];
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler API_AVAILABLE(ios(10.0))
{
[self sendEvent:@{@"type": @"notification", @"notification": [self payloadFromNotification:notification]}];
if (self.previousNotificationDelegate != nil
&& self.previousNotificationDelegate != self
&& [self.previousNotificationDelegate respondsToSelector:@selector(userNotificationCenter:willPresentNotification:withCompletionHandler:)]) {
[self.previousNotificationDelegate userNotificationCenter:center willPresentNotification:notification withCompletionHandler:completionHandler];
return;
}
completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionBadge);
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler API_AVAILABLE(ios(10.0))
{
NSDictionary *payload = [self payloadFromNotification:response.notification];
self.initialNotification = payload;
[self sendEvent:@{@"type": @"notificationOpened", @"notification": payload}];
if (self.previousNotificationDelegate != nil
&& self.previousNotificationDelegate != self
&& [self.previousNotificationDelegate respondsToSelector:@selector(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:)]) {
[self.previousNotificationDelegate userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler];
return;
}
completionHandler();
}
- (NSDictionary *)payloadFromNotification:(UNNotification *)notification API_AVAILABLE(ios(10.0))
{
NSMutableDictionary *payload = [NSMutableDictionary dictionary];
NSDictionary *userInfo = notification.request.content.userInfo ? notification.request.content.userInfo : @{};
payload[@"data"] = userInfo;
if (notification.request.identifier.length > 0) {
payload[@"identifier"] = notification.request.identifier;
}
NSMutableDictionary *apsNotification = [NSMutableDictionary dictionary];
if (notification.request.content.title.length > 0) {
apsNotification[@"title"] = notification.request.content.title;
}
if (notification.request.content.body.length > 0) {
apsNotification[@"body"] = notification.request.content.body;
}
if (notification.request.content.categoryIdentifier.length > 0) {
apsNotification[@"category"] = notification.request.content.categoryIdentifier;
}
if (apsNotification.count > 0) {
payload[@"notification"] = apsNotification;
}
return payload;
}
- (void)sendEvent:(NSDictionary *)event
{
if (self.notificationCallbackId.length == 0 || event == nil) {
return;
}
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:event];
[result setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:result callbackId:self.notificationCallbackId];
}
- (NSDictionary *)firstDictionaryArgument:(CDVInvokedUrlCommand *)command
{
id first = [command.arguments firstObject];
if ([first isKindOfClass:[NSDictionary class]]) {
return first;
}
return @{};
}
- (NSString *)stringFromDictionary:(NSDictionary *)dictionary key:(NSString *)key
{
id value = dictionary[key];
if ([value isKindOfClass:[NSString class]]) {
return value;
}
if ([value respondsToSelector:@selector(stringValue)]) {
return [value stringValue];
}
return @"";
}
- (BOOL)boolFromDictionary:(NSDictionary *)dictionary key:(NSString *)key defaultValue:(BOOL)defaultValue
{
id value = dictionary[key];
if ([value isKindOfClass:[NSNumber class]]) {
return [value boolValue];
}
if ([value isKindOfClass:[NSString class]]) {
NSString *lowercase = [value lowercaseString];
if ([lowercase isEqualToString:@"true"] || [lowercase isEqualToString:@"1"] || [lowercase isEqualToString:@"yes"]) {
return YES;
}
if ([lowercase isEqualToString:@"false"] || [lowercase isEqualToString:@"0"] || [lowercase isEqualToString:@"no"]) {
return NO;
}
}
return defaultValue;
}
- (NSString *)infoStringForKey:(NSString *)key
{
id value = [[NSBundle mainBundle] objectForInfoDictionaryKey:key];
if ([value isKindOfClass:[NSString class]]) {
return value;
}
return @"";
}
- (void)sendSuccessForCommand:(CDVInvokedUrlCommand *)command dictionary:(NSDictionary *)dictionary
{
CDVPluginResult *result = dictionary == nil
? [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]
: [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
- (void)sendErrorForCommand:(CDVInvokedUrlCommand *)command code:(NSString *)code message:(NSString *)message
{
NSDictionary *error = @{@"code": code ? code : @"ERROR", @"message": message ? message : @""};
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:error];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
@end