Skip to content

Commit b29c4d2

Browse files
committed
🔥 Implement pure APNS to Firebase token converter
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.
1 parent 859deee commit b29c4d2

File tree

3 files changed

+149
-0
lines changed

3 files changed

+149
-0
lines changed

Sources/FCM/FCM.swift

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public struct FCM {
1212
let scope = "https://www.googleapis.com/auth/cloud-platform"
1313
let audience = "https://www.googleapis.com/oauth2/v4/token"
1414
let actionsBaseURL = "https://fcm.googleapis.com/v1/projects/"
15+
let iidURL = "https://iid.googleapis.com/iid/v1:"
1516

1617
// MARK: Default configurations
1718

Sources/FCM/FCMConfiguration.swift

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import Vapor
44
public struct FCMConfiguration {
55
let email, projectId, key: String
66

7+
let serverKey = Environment.get("FCM_SERVER_KEY")
8+
79
// MARK: Default configurations
810

911
public var apnsDefaultConfig: FCMApnsConfig<FCMApnsPayload>?
+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import Foundation
2+
import Vapor
3+
4+
public struct RegisterAPNSID {
5+
let appBundleId: String
6+
let serverKey: String?
7+
let sandbox: Bool
8+
9+
public init (appBundleId: String, serverKey: String? = nil, sandbox: Bool = false) {
10+
self.appBundleId = appBundleId
11+
self.serverKey = serverKey
12+
self.sandbox = sandbox
13+
}
14+
}
15+
16+
extension RegisterAPNSID {
17+
public static var env: RegisterAPNSID {
18+
guard let appBundleId = Environment.get("FCM_APP_BUNDLE_ID") else {
19+
fatalError("FCM: Register APNS: missing FCM_APP_BUNDLE_ID environment variable")
20+
}
21+
return .init(appBundleId: appBundleId)
22+
}
23+
}
24+
25+
public struct APNSToFirebaseToken {
26+
public let registration_token, apns_token: String
27+
public let isRegistered: Bool
28+
}
29+
30+
extension FCM {
31+
/// Helper method which registers your pure APNS token in Firebase Cloud Messaging
32+
/// and returns firebase tokens for each APNS token
33+
///
34+
/// Convenient way
35+
///
36+
/// Declare `RegisterAPNSID` via extension
37+
/// ```swift
38+
/// extension RegisterAPNSID {
39+
/// static var myApp: RegisterAPNSID { .init(appBundleId: "com.myapp") }
40+
/// }
41+
/// ```
42+
///
43+
public func registerAPNS(
44+
_ id: RegisterAPNSID,
45+
tokens: String...,
46+
on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> {
47+
registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop)
48+
}
49+
50+
/// Helper method which registers your pure APNS token in Firebase Cloud Messaging
51+
/// and returns firebase tokens for each APNS token
52+
///
53+
/// Convenient way
54+
///
55+
/// Declare `RegisterAPNSID` via extension
56+
/// ```swift
57+
/// extension RegisterAPNSID {
58+
/// static var myApp: RegisterAPNSID { .init(appBundleId: "com.myapp") }
59+
/// }
60+
/// ```
61+
///
62+
public func registerAPNS(
63+
_ id: RegisterAPNSID,
64+
tokens: [String],
65+
on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> {
66+
registerAPNS(appBundleId: id.appBundleId, serverKey: id.serverKey, sandbox: id.sandbox, tokens: tokens, on: eventLoop)
67+
}
68+
69+
/// Helper method which registers your pure APNS token in Firebase Cloud Messaging
70+
/// and returns firebase tokens for each APNS token
71+
public func registerAPNS(
72+
appBundleId: String,
73+
serverKey: String? = nil,
74+
sandbox: Bool = false,
75+
tokens: String...,
76+
on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> {
77+
registerAPNS(appBundleId: appBundleId, serverKey: serverKey, sandbox: sandbox, tokens: tokens, on: eventLoop)
78+
}
79+
80+
/// Helper method which registers your pure APNS token in Firebase Cloud Messaging
81+
/// and returns firebase tokens for each APNS token
82+
public func registerAPNS(
83+
appBundleId: String,
84+
serverKey: String? = nil,
85+
sandbox: Bool = false,
86+
tokens: [String],
87+
on eventLoop: EventLoop? = nil) -> EventLoopFuture<[APNSToFirebaseToken]> {
88+
let eventLoop = eventLoop ?? application.eventLoopGroup.next()
89+
guard tokens.count <= 100 else {
90+
return eventLoop.makeFailedFuture(Abort(.internalServerError, reason: "FCM: Register APNS: tokens count should be less or equeal 100"))
91+
}
92+
guard tokens.count > 0 else {
93+
return eventLoop.future([])
94+
}
95+
guard let configuration = self.configuration else {
96+
#if DEBUG
97+
fatalError("FCM not configured. Use app.fcm.configuration = ...")
98+
#else
99+
return eventLoop.future([])
100+
#endif
101+
}
102+
guard let serverKey = serverKey ?? configuration.serverKey else {
103+
fatalError("FCM: Register APNS: Server Key is missing.")
104+
}
105+
let url = iidURL + "batchImport"
106+
return eventLoop.future().flatMapThrowing { accessToken throws -> HTTPClient.Request in
107+
struct Payload: Codable {
108+
let application: String
109+
let sandbox: Bool
110+
let apns_tokens: [String]
111+
}
112+
let payload = Payload(application: appBundleId, sandbox: false, apns_tokens: tokens)
113+
let payloadData = try JSONEncoder().encode(payload)
114+
115+
var headers = HTTPHeaders()
116+
headers.add(name: "Authorization", value: "key=\(serverKey)")
117+
headers.add(name: "Content-Type", value: "application/json")
118+
119+
return try .init(url: url, method: .POST, headers: headers, body: .data(payloadData))
120+
}.flatMap { request in
121+
return self.client.execute(request: request).flatMapThrowing { res in
122+
guard 200 ..< 300 ~= res.status.code else {
123+
guard
124+
let bb = res.body,
125+
let bytes = bb.getBytes(at: 0, length: bb.readableBytes),
126+
let reason = String(bytes: bytes, encoding: .utf8) else {
127+
throw Abort(.internalServerError, reason: "FCM: Register APNS: unable to decode error response")
128+
}
129+
throw Abort(.internalServerError, reason: reason)
130+
}
131+
struct Result: Codable {
132+
struct Result: Codable {
133+
let registration_token, apns_token, status: String
134+
}
135+
var results: [Result]
136+
}
137+
guard let body = res.body, let result = try? JSONDecoder().decode(Result.self, from: body) else {
138+
throw Abort(.notFound, reason: "FCM: Register APNS: empty response")
139+
}
140+
return result.results.map {
141+
.init(registration_token: $0.registration_token, apns_token: $0.apns_token, isRegistered: $0.status == "OK")
142+
}
143+
}
144+
}
145+
}
146+
}

0 commit comments

Comments
 (0)