Swiftpack.co - Package - MihaelIsaev/FCM

Mihael Isaev

MIT License Swift 4.1 Twitter


Intro 👏

It's a swift lib that gives ability to send push notifications through Firebase Cloud Messaging.

Built for Vapor3 and depends on JWT Vapor lib.

Note: the project is in active development state and it may cause huge syntax changes before v1.0.0

If you have great ideas of how to improve this package write me (@iMike) in Vapor's discord chat or just send pull request.

Hope it'll be useful for someone :)

Install through Swift Package Manager ❤️

Edit your Package.swift

//add this repo to dependencies
.package(url: "https://github.com/MihaelIsaev/FCM.git", from: "0.6.2")
//and don't forget about targets
//"FCM"

How it works ?

First of all you should configure FCM in configure.swift

import FCM

/// Called before your application initializes.
public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
//here you should initialize FCM
}

There are two ways

1. Using environment variables 👍
let fcm = FCM()
services.register(fcm, as: FCM.self)

and don't forget to pass the following environment variables

fcmServiceAccountKeyPath // /tmp/serviceAccountKey.json

OR

fcmEmail // firebase-adminsdk-0w4ba@example-3ab5c.iam.gserviceaccount.com
fcmKeyPath // /tmp/fcm.pem
fcmProjectId // example-3ab5c
2. Manually 🤖
let fcm = FCM(pathToServiceAccountKey: "/tmp/serviceAccountKey.json")
services.register(fcm, as: FCM.self)

OR

let fcm = FCM(email: "firebase-adminsdk-0w4ba@example-3ab5c.iam.gserviceaccount.com",
              projectId: "example-3ab5c",
              pathToKey: "/tmp/fcm.pem")
services.register(fcm, as: FCM.self)

OR

let fcm = FCM(email: "firebase-adminsdk-0w4ba@example-3ab5c.iam.gserviceaccount.com",
              projectId: "example-3ab5c",
              key: "<YOUR PRIVATE KEY>")
services.register(fcm, as: FCM.self)

⚠️ TIP: serviceAccountKey.json you could get from Firebase Console

🔑 Just go to Settings -> Service Accounts tab and press Create Private Key button in e.g. NodeJS tab

OPTIONAL: Set default configurations, e.g. to enable notification sound

Add the following code to your configure.swift

fcm.apnsDefaultConfig = FCMApnsConfig(headers: [:],
                                      aps: FCMApnsApsObject(sound: "default"))
fcm.androidDefaultConfig = FCMAndroidConfig(ttl: "86400s",
                                            restricted_package_name: "com.example.myapp",
                                            notification: FCMAndroidNotification(sound: "default"))
fcm.webpushDefaultConfig = FCMWebpushConfig(headers: [:],
                                            data: [:],
                                            notification: [:])

Let's send first push notification! 🚀

Then you could send push notifications using token, topic or condition.

Here's an example route handler with push notification sending using token

router.get("testfcm") { req -> Future<String> in
  let fcm = try req.make(FCM.self)
  let token = "<YOUR FIREBASE DEVICE TOKEN>"
  let notification = FCMNotification(title: "Vapor is awesome!", body: "Swift one love! ❤️")
  let message = FCMMessage(token: token, notification: notification)
  return try fcm.sendMessage(req.client(), message: message)
}

fcm.sendMessage returns message name like projects/example-3ab5c/messages/1531222329648135

FCMMessage struct is absolutely the same as Message struct in Firebase docs https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages So you could take a look on its source code to build proper message.

Bonus 🍾

In my Vapor projects I'm using this extension for sending push notifications

import Vapor
import Fluent
import FCM

protocol Firebaseable: Model {
    var firebaseToken: String? { get set }
}

extension Firebaseable {
    func sendPush(title: String, message: String, on req: Container) throws -> Future<Void> {
        guard let token = firebaseToken else {
            return req.eventLoop.newSucceededFuture(result: ())
        }
        return try Self.sendPush(title: title, message: message, token: token, on: req)
    }
    
    static func sendPush(title: String, message: String, token: String, on container: Container) throws -> Future<Void> {
        let fcm = try container.make(FCM.self)
        let message = FCMMessage(token: token, notification: FCMNotification(title: title, body: message))
        return try fcm.sendMessage(container.make(Client.self), message: message).transform(to: ())
    }
}

extension Array where Element == Firebaseable {
    func sendPush(title: String, message: String, on container: Container) throws -> Future<Void> {
        return try map { try $0.sendPush(title: title, message: message, on: container) }.flatten(on: container)
    }
}

Optionally you can handle sendMessage error through defining catchFlatMap after it, e.g. for removing broken tokens or anything else

return try fcm.sendMessage(container.make(Client.self), message: message).transform(to: ()).catchFlatMap { error in
    guard let googleError = error as? GoogleError, let fcmError = googleError.fcmError else {
        return container.eventLoop.newSucceededFuture(result: ())
    }
    switch fcmError.errorCode {
        case .unregistered: // drop token only if unregistered
            return container.requestPooledConnection(to: .psql).flatMap { conn in
                return Self.query(on: conn).filter(\.firebaseToken == token).first().flatMap { model in
                    defer { try? container.releasePooledConnection(conn, to: .psql) }
                    guard var model = model else { return container.eventLoop.newSucceededFuture(result: ()) }
                    model.firebaseToken = nil
                    return model.save(on: conn).transform(to: ())
                }.always {
                    try? container.releasePooledConnection(conn, to: .psql)
                }
            }
        default:
            return container.eventLoop.newSucceededFuture(result: ())
    }
}

Special thanks to @grahamburgsma for GoogleError and FCMError #10

Then e.g. I'm conforming my Token model to Firebaseable

final class Token: Content {
    var id: UUID?
    var token: String
    var userId: User.ID
    var firebaseToken: String?
}
extension Token: Firebaseable {}

So then you'll be able to send pushes by querying tokens like this

Token.query(on: req)
    .join(\User.id, to: \Token.userId)
    .filter(\User.email == "benny@gmail.com")
    .all().map { tokens in
    try tokens.sendPush(title: "Test push", message: "Hello world!", on: req)
}

Github

link
Stars: 32
Help us keep the lights on

Dependencies

Releases

0.7.0 - Mar 11, 2019

Many thanks to @grahamburgsma

So, now we can parse Google and FCM errors, let's take a look how

return try fcm.sendMessage(container.make(Client.self), message: message).transform(to: ()).catchFlatMap { error in
    guard let googleError = error as? GoogleError, let fcmError = googleError.fcmError else {
        return container.eventLoop.newFailedFuture(error: error)
    }
    switch fcmError.errorCode {
    case .unspecified: break
    case .invalid: break
    case .unregistered: break
    case .senderIDMismatch: break
    case .quotaExceeded: break
    case .apnsAuth: break
    case .unavailable: break
    case .internal: break
    }
}

And now we can delete token if we're getting unregistered error like this:

return try fcm.sendMessage(container.make(Client.self), message: message).transform(to: ()).catchFlatMap { error in
    guard let googleError = error as? GoogleError, let fcmError = googleError.fcmError else {
        return container.eventLoop.newSucceededFuture(result: ())
    }
    switch fcmError.errorCode {
        case .unregistered: // drop token only if unregistered
            return container.requestPooledConnection(to: .psql).flatMap { conn in
                return Self.query(on: conn).filter(\.firebaseToken == token).first().flatMap { model in
                    defer { try? container.releasePooledConnection(conn, to: .psql) }
                    guard var model = model else { return container.eventLoop.newSucceededFuture(result: ()) }
                    model.firebaseToken = nil
                    return model.save(on: conn).transform(to: ())
                }.always {
                    try? container.releasePooledConnection(conn, to: .psql)
                }
            }
        default:
            return container.eventLoop.newSucceededFuture(result: ())
    }
}

0.6.2 - Oct 2, 2018

0.6.1 - Oct 2, 2018

This is fixing unexpected behavior when the server is unable to send pushes after an hour after launch.