From b29c4d239c4b12621a3e2f834789c924bda93000 Mon Sep 17 00:00:00 2001 From: Mihael Isaev Date: Fri, 15 May 2020 18:19:02 +0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=20Implement=20pure=20APNS=20to=20F?= =?UTF-8?q?irebase=20token=20converter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So now in your iOS app you can throw away Firebase libs from dependencies cause you can send pure APNS token to your server app and it will register it in Firebase by itself. It is must have for developers who don't want to add Firebase libs into their apps, and especially for iOS projects who use Swift Package Manager cause Firebase doesn't have SPM support for its libs yet. --- Sources/FCM/FCM.swift | 1 + Sources/FCM/FCMConfiguration.swift | 2 + Sources/FCM/Helpers/FCM+RegisterAPNS.swift | 146 +++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 Sources/FCM/Helpers/FCM+RegisterAPNS.swift diff --git a/Sources/FCM/FCM.swift b/Sources/FCM/FCM.swift index 73ddae2..3867c4d 100644 --- a/Sources/FCM/FCM.swift +++ b/Sources/FCM/FCM.swift @@ -12,6 +12,7 @@ public struct FCM { let scope = "https://www.googleapis.com/auth/cloud-platform" let audience = "https://www.googleapis.com/oauth2/v4/token" let actionsBaseURL = "https://fcm.googleapis.com/v1/projects/" + let iidURL = "https://iid.googleapis.com/iid/v1:" // MARK: Default configurations diff --git a/Sources/FCM/FCMConfiguration.swift b/Sources/FCM/FCMConfiguration.swift index ee739cc..e3ffb44 100644 --- a/Sources/FCM/FCMConfiguration.swift +++ b/Sources/FCM/FCMConfiguration.swift @@ -4,6 +4,8 @@ import Vapor public struct FCMConfiguration { let email, projectId, key: String + let serverKey = Environment.get("FCM_SERVER_KEY") + // MARK: Default configurations public var apnsDefaultConfig: FCMApnsConfig? diff --git a/Sources/FCM/Helpers/FCM+RegisterAPNS.swift b/Sources/FCM/Helpers/FCM+RegisterAPNS.swift new file mode 100644 index 0000000..a0f6fb4 --- /dev/null +++ b/Sources/FCM/Helpers/FCM+RegisterAPNS.swift @@ -0,0 +1,146 @@ +import Foundation +import Vapor + +public struct RegisterAPNSID { + let appBundleId: String + let serverKey: String? + let sandbox: Bool + + public init (appBundleId: String, serverKey: String? = nil, sandbox: Bool = false) { + self.appBundleId = appBundleId + self.serverKey = serverKey + self.sandbox = sandbox + } +} + +extension RegisterAPNSID { + public static var env: RegisterAPNSID { + guard let appBundleId = Environment.get("FCM_APP_BUNDLE_ID") else { + fatalError("FCM: Register APNS: missing FCM_APP_BUNDLE_ID environment variable") + } + return .init(appBundleId: appBundleId) + } +} + +public struct APNSToFirebaseToken { + public let registration_token, apns_token: String + public let isRegistered: Bool +} + +extension FCM { + /// Helper method which registers your pure APNS token in Firebase Cloud Messaging + /// and returns firebase tokens for each APNS token + /// + /// Convenient way + /// + /// Declare `RegisterAPNSID` via extension + /// ```swift + /// extension RegisterAPNSID { + /// static var myApp: RegisterAPNSID { .init(appBundleId: "com.myapp") } + /// } + /// ``` + /// + public func registerAPNS( + _ id: RegisterAPNSID, + tokens: String..., + on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { + registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop) + } + + /// Helper method which registers your pure APNS token in Firebase Cloud Messaging + /// and returns firebase tokens for each APNS token + /// + /// Convenient way + /// + /// Declare `RegisterAPNSID` via extension + /// ```swift + /// extension RegisterAPNSID { + /// static var myApp: RegisterAPNSID { .init(appBundleId: "com.myapp") } + /// } + /// ``` + /// + public func registerAPNS( + _ id: RegisterAPNSID, + tokens: [String], + on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { + registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop) + } + + /// Helper method which registers your pure APNS token in Firebase Cloud Messaging + /// and returns firebase tokens for each APNS token + public func registerAPNS( + appBundleId: String, + serverKey: String? = nil, + sandbox: Bool = false, + tokens: String..., + on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { + registerAPNS(appBundleId: appBundleId, serverKey: serverKey, sandbox: sandbox, tokens: tokens, on: eventLoop) + } + + /// Helper method which registers your pure APNS token in Firebase Cloud Messaging + /// and returns firebase tokens for each APNS token + public func registerAPNS( + appBundleId: String, + serverKey: String? = nil, + sandbox: Bool = false, + tokens: [String], + on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> { + let eventLoop = eventLoop ?? application.eventLoopGroup.next() + guard tokens.count <= 100 else { + return eventLoop.makeFailedFuture(Abort(.internalServerError, reason: "FCM: Register APNS: tokens count should be less or equeal 100")) + } + guard tokens.count > 0 else { + return eventLoop.future([]) + } + guard let configuration = self.configuration else { + #if DEBUG + fatalError("FCM not configured. Use app.fcm.configuration = ...") + #else + return eventLoop.future([]) + #endif + } + guard let serverKey = serverKey ?? configuration.serverKey else { + fatalError("FCM: Register APNS: Server Key is missing.") + } + let url = iidURL + "batchImport" + return eventLoop.future().flatMapThrowing { accessToken throws -> HTTPClient.Request in + struct Payload: Codable { + let application: String + let sandbox: Bool + let apns_tokens: [String] + } + let payload = Payload(application: appBundleId, sandbox: false, apns_tokens: tokens) + let payloadData = try JSONEncoder().encode(payload) + + var headers = HTTPHeaders() + headers.add(name: "Authorization", value: "key=\(serverKey)") + headers.add(name: "Content-Type", value: "application/json") + + return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData)) + }.flatMap { request in + return self.client.execute(request: request).flatMapThrowing { res in + guard 200 ..< 300 ~= res.status.code else { + guard + let bb = res.body, + let bytes = bb.getBytes(at: 0, length: bb.readableBytes), + let reason = String(bytes: bytes, encoding: .utf8) else { + throw Abort(.internalServerError, reason: "FCM: Register APNS: unable to decode error response") + } + throw Abort(.internalServerError, reason: reason) + } + struct Result: Codable { + struct Result: Codable { + let registration_token, apns_token, status: String + } + var results: [Result] + } + guard let body = res.body, let result = try? JSONDecoder().decode(Result.self, from: body) else { + throw Abort(.notFound, reason: "FCM: Register APNS: empty response") + } + return result.results.map { + .init(registration_token: $0.registration_token, apns_token: $0.apns_token, isRegistered: $0.status == "OK") + } + } + } + } +}