aboutsummaryrefslogtreecommitdiffhomepage
path: root/Example/Messaging/App
diff options
context:
space:
mode:
Diffstat (limited to 'Example/Messaging/App')
-rw-r--r--Example/Messaging/App/AppDelegate.swift114
-rw-r--r--Example/Messaging/App/Base.lproj/LaunchScreen.storyboard27
-rw-r--r--Example/Messaging/App/Base.lproj/Main.storyboard48
-rw-r--r--Example/Messaging/App/Data+MessagingExtensions.swift25
-rw-r--r--Example/Messaging/App/Environment.swift28
-rw-r--r--Example/Messaging/App/GoogleService-Info.plist30
-rw-r--r--Example/Messaging/App/Messaging-Info.plist53
-rw-r--r--Example/Messaging/App/MessagingViewController.swift332
-rw-r--r--Example/Messaging/App/Messaging_Example.entitlements8
-rw-r--r--Example/Messaging/App/NotificationsController.swift132
10 files changed, 797 insertions, 0 deletions
diff --git a/Example/Messaging/App/AppDelegate.swift b/Example/Messaging/App/AppDelegate.swift
new file mode 100644
index 0000000..0f40a4e
--- /dev/null
+++ b/Example/Messaging/App/AppDelegate.swift
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import UIKit
+import FirebaseDev
+import UserNotifications
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ var window: UIWindow?
+
+ static let isWithinUnitTest: Bool = {
+ if let testClass = NSClassFromString("XCTestCase") {
+ return true
+ } else {
+ return false
+ }
+ }()
+
+ static var hasPresentedInvalidServiceInfoPlistAlert = false
+
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
+ guard !AppDelegate.isWithinUnitTest else {
+ // During unit tests, we don't want to initialize Firebase, since by default we want to able
+ // to run unit tests without requiring a non-dummy GoogleService-Info.plist file
+ return true
+ }
+
+ guard SampleAppUtilities.appContainsRealServiceInfoPlist() else {
+ // We can't run because the GoogleService-Info.plist file is likely the dummy file which needs
+ // to be replaced with a real one, or somehow the file has been removed from the app bundle.
+ // See: https://github.com/firebase/firebase-ios-sdk/
+ // We'll present a friendly alert when the app becomes active.
+ return true
+ }
+
+ FirebaseApp.configure()
+ Messaging.messaging().delegate = self
+
+ NotificationsController.configure()
+
+ if #available(iOS 8.0, *) {
+ // Always register for remote notifications. This will not show a prompt to the user, as by
+ // default it will provision silent notifications. We can use UNUserNotificationCenter to
+ // request authorization for user-facing notifications.
+ application.registerForRemoteNotifications()
+ } else {
+ // iOS 7 didn't differentiate between user-facing and other notifications, so we should just
+ // register for remote notifications
+ NotificationsController.shared.registerForUserFacingNotificationsFor(application)
+ }
+
+ printFCMToken()
+ return true
+ }
+
+ func printFCMToken() {
+ if let token = Messaging.messaging().fcmToken {
+ print("FCM Token: \(token)")
+ } else {
+ print("FCM Token: nil")
+ }
+ }
+
+ func application(_ application: UIApplication,
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
+ print("APNS Token: \(deviceToken.hexByteString)")
+ NotificationCenter.default.post(name: APNSTokenReceivedNotification, object: nil)
+ if #available(iOS 8.0, *) {
+ } else {
+ // On iOS 7, receiving a device token also means our user notifications were granted, so fire
+ // the notification to update our user notifications UI
+ NotificationCenter.default.post(name: UserNotificationsChangedNotification, object: nil)
+ }
+ }
+
+ func application(_ application: UIApplication,
+ didRegister notificationSettings: UIUserNotificationSettings) {
+ NotificationCenter.default.post(name: UserNotificationsChangedNotification, object: nil)
+ }
+
+ func applicationDidBecomeActive(_ application: UIApplication) {
+ // If the app didn't start property due to an invalid GoogleService-Info.plist file, show an
+ // alert to the developer.
+ if !SampleAppUtilities.appContainsRealServiceInfoPlist() &&
+ !AppDelegate.hasPresentedInvalidServiceInfoPlistAlert {
+ if let vc = window?.rootViewController {
+ SampleAppUtilities.presentAlertForInvalidServiceInfoPlistFrom(vc)
+ AppDelegate.hasPresentedInvalidServiceInfoPlistAlert = true
+ }
+ }
+ }
+}
+
+extension AppDelegate: MessagingDelegate {
+ func messaging(_ messaging: Messaging, didRefreshRegistrationToken fcmToken: String) {
+ printFCMToken()
+ }
+}
+
diff --git a/Example/Messaging/App/Base.lproj/LaunchScreen.storyboard b/Example/Messaging/App/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..fdf3f97
--- /dev/null
+++ b/Example/Messaging/App/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11106"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Messaging/App/Base.lproj/Main.storyboard b/Example/Messaging/App/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..6df1a82
--- /dev/null
+++ b/Example/Messaging/App/Base.lproj/Main.storyboard
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="taE-sK-BOl">
+ <device id="retina4_7" orientation="portrait">
+ <adaptation id="fullscreen"/>
+ </device>
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <scenes>
+ <!--Firebase Cloud Messaging-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="MessagingViewController" customModule="Messaging_Example" customModuleProvider="target" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ </view>
+ <navigationItem key="navigationItem" title="Firebase Cloud Messaging" id="z1u-kE-qKb"/>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="698" y="164"/>
+ </scene>
+ <!--Navigation Controller-->
+ <scene sceneID="rmF-xz-rwn">
+ <objects>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="Ju1-Bj-8eG" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ <navigationController id="taE-sK-BOl" sceneMemberID="viewController">
+ <navigationBar key="navigationBar" contentMode="scaleToFill" id="iTL-Kg-11w">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
+ <autoresizingMask key="autoresizingMask"/>
+ </navigationBar>
+ <connections>
+ <segue destination="BYZ-38-t0r" kind="relationship" relationship="rootViewController" id="04R-HZ-bi6"/>
+ </connections>
+ </navigationController>
+ </objects>
+ <point key="canvasLocation" x="-92" y="165"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Messaging/App/Data+MessagingExtensions.swift b/Example/Messaging/App/Data+MessagingExtensions.swift
new file mode 100644
index 0000000..99ded25
--- /dev/null
+++ b/Example/Messaging/App/Data+MessagingExtensions.swift
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+
+extension Data {
+ // Print Data as a string of bytes in hex, such as the common representation of APNs device tokens
+ // See: http://stackoverflow.com/a/40031342/9849
+ var hexByteString: String {
+ return self.map { String(format: "%02.2hhx", $0) }.joined()
+ }
+}
diff --git a/Example/Messaging/App/Environment.swift b/Example/Messaging/App/Environment.swift
new file mode 100644
index 0000000..5219c64
--- /dev/null
+++ b/Example/Messaging/App/Environment.swift
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+
+struct Environment {
+ static let isSimulator: Bool = {
+ var isSim = false
+ #if arch(i386) || arch(x86_64)
+ isSim = true
+ #endif
+
+ return isSim
+ }()
+}
diff --git a/Example/Messaging/App/GoogleService-Info.plist b/Example/Messaging/App/GoogleService-Info.plist
new file mode 100644
index 0000000..89afffe
--- /dev/null
+++ b/Example/Messaging/App/GoogleService-Info.plist
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>API_KEY</key>
+ <string>correct_api_key</string>
+ <key>TRACKING_ID</key>
+ <string>correct_tracking_id</string>
+ <key>CLIENT_ID</key>
+ <string>correct_client_id</string>
+ <key>REVERSED_CLIENT_ID</key>
+ <string>correct_reversed_client_id</string>
+ <key>ANDROID_CLIENT_ID</key>
+ <string>correct_android_client_id</string>
+ <key>GOOGLE_APP_ID</key>
+ <string>1:123:ios:123abc</string>
+ <key>GCM_SENDER_ID</key>
+ <string>correct_gcm_sender_id</string>
+ <key>PLIST_VERSION</key>
+ <string>1</string>
+ <key>BUNDLE_ID</key>
+ <string>com.google.FirebaseSDKTests</string>
+ <key>PROJECT_ID</key>
+ <string>abc-xyz-123</string>
+ <key>DATABASE_URL</key>
+ <string>https://abc-xyz-123.firebaseio.com</string>
+ <key>STORAGE_BUCKET</key>
+ <string>project-id-123.storage.firebase.com</string>
+</dict>
+</plist>
diff --git a/Example/Messaging/App/Messaging-Info.plist b/Example/Messaging/App/Messaging-Info.plist
new file mode 100644
index 0000000..e42f39d
--- /dev/null
+++ b/Example/Messaging/App/Messaging-Info.plist
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UIBackgroundModes</key>
+ <array>
+ <string>remote-notification</string>
+ </array>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/Example/Messaging/App/MessagingViewController.swift b/Example/Messaging/App/MessagingViewController.swift
new file mode 100644
index 0000000..00ed3ff
--- /dev/null
+++ b/Example/Messaging/App/MessagingViewController.swift
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import UIKit
+
+import FirebaseDev
+
+enum Row: String {
+ case apnsToken = "apnsToken"
+ case apnsStatus = "apnsStatus"
+ case requestAPNSPermissions = "requestAPNSPermissions"
+ case fcmToken = "fcmToken"
+}
+
+enum PermissionsButtonTitle: String {
+ case requestPermissions = "Request User Notifications"
+ case noAPNS = "Cannot Request Permissions (No APNs)"
+ case alreadyRequested = "Already Requested Permissions"
+ case simulator = "Cannot Request Permissions (Simulator)"
+}
+
+class MessagingViewController: UIViewController {
+
+ let tableView: UITableView
+
+ var sections = [[Row]]()
+ var sectionHeaderTitles = [String?]()
+
+ var allowedNotificationTypes: [NotificationsControllerAllowedNotificationType]?
+
+ // Cached rows by Row type. Since this is largely a fixed table view, we'll
+ // keep track of our created cells and UI, rather than have all the logic
+
+ required init?(coder aDecoder: NSCoder) {
+ tableView = UITableView(frame: CGRect.zero, style: .grouped)
+ tableView.rowHeight = UITableViewAutomaticDimension
+ tableView.estimatedRowHeight = 44
+ // Allow UI Controls within the table to be immediately responsive
+ tableView.delaysContentTouches = false
+ super.init(coder: aDecoder)
+ tableView.dataSource = self
+ tableView.delegate = self
+ }
+
+ override func loadView() {
+ super.loadView()
+ view = UIView(frame: CGRect.zero)
+ view.addSubview(self.tableView)
+ // Ensure that the tableView always is the size of the view
+ tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ let center = NotificationCenter.default
+ center.addObserver(self,
+ selector: #selector(onAPNSTokenReceived) ,
+ name: APNSTokenReceivedNotification,
+ object: nil)
+ center.addObserver(self,
+ selector: #selector(onUserNotificationSettingsChanged),
+ name: UserNotificationsChangedNotification,
+ object: nil)
+ center.addObserver(self,
+ selector: #selector(onFCMTokenRefreshed),
+ name: Notification.Name.MessagingRegistrationTokenRefreshed,
+ object: nil)
+ updateAllowedNotificationTypes {
+ self.resetTableContents()
+ self.tableView.reloadData()
+ }
+ }
+
+ func onAPNSTokenReceived() {
+ // Reload the appropriate cells
+ updateAllowedNotificationTypes {
+ if let tokenPath = self.indexPathFor(.apnsToken),
+ let statusPath = self.indexPathFor(.apnsStatus),
+ let requestPath = self.indexPathFor(.requestAPNSPermissions) {
+ self.updateIndexPaths(indexPaths: [tokenPath, statusPath, requestPath])
+ }
+ }
+ }
+
+ func onFCMTokenRefreshed() {
+ if let indexPath = indexPathFor(.fcmToken) {
+ updateIndexPaths(indexPaths: [indexPath])
+ }
+ }
+
+ func onUserNotificationSettingsChanged() {
+ updateAllowedNotificationTypes {
+ if let statusPath = self.indexPathFor(.apnsStatus),
+ let requestPath = self.indexPathFor(.requestAPNSPermissions) {
+ self.updateIndexPaths(indexPaths: [statusPath, requestPath])
+ }
+ }
+ }
+
+ private func updateIndexPaths(indexPaths: [IndexPath]) {
+ tableView.beginUpdates()
+ tableView.reloadRows(at: indexPaths, with: .none)
+ tableView.endUpdates()
+ }
+
+ fileprivate func updateAllowedNotificationTypes(_ completion: (() -> Void)?) {
+ NotificationsController.shared.getAllowedNotificationTypes { (types) in
+ self.allowedNotificationTypes = types
+ self.updateRequestAPNSButton()
+ completion?()
+ }
+ }
+
+ fileprivate func updateRequestAPNSButton() {
+ guard !Environment.isSimulator else {
+ requestPermissionsButton.isEnabled = false
+ requestPermissionsButton.setTitle(PermissionsButtonTitle.simulator.rawValue, for: .normal)
+ return
+ }
+ guard let allowedTypes = allowedNotificationTypes else {
+ requestPermissionsButton.isEnabled = false
+ requestPermissionsButton.setTitle(PermissionsButtonTitle.noAPNS.rawValue, for: .normal)
+ return
+ }
+
+ requestPermissionsButton.isEnabled =
+ (allowedTypes.count == 1 && allowedTypes.first! == .silent)
+
+ let title: PermissionsButtonTitle =
+ (requestPermissionsButton.isEnabled ? .requestPermissions : .alreadyRequested)
+ requestPermissionsButton.setTitle(title.rawValue, for: .normal)
+ }
+
+ // MARK: UI (Cells and Buttons) Defined as lazy properties
+ lazy var apnsTableCell: UITableViewCell = {
+ let cell = UITableViewCell(style: .subtitle, reuseIdentifier: Row.apnsToken.rawValue)
+ cell.textLabel?.numberOfLines = 0
+ cell.textLabel?.lineBreakMode = .byWordWrapping
+ return cell
+ }()
+
+ lazy var apnsStatusTableCell: UITableViewCell = {
+ let cell = UITableViewCell(style: UITableViewCellStyle.value1, reuseIdentifier: Row.apnsStatus.rawValue)
+ cell.textLabel?.text = "Allowed:"
+ cell.detailTextLabel?.numberOfLines = 0
+ cell.detailTextLabel?.lineBreakMode = .byWordWrapping
+ return cell
+ }()
+
+ lazy var requestPermissionsButton: UIButton = {
+ let button = UIButton(type: .system)
+ button.setTitle(PermissionsButtonTitle.requestPermissions.rawValue, for: .normal)
+ button.setTitleColor(UIColor.gray, for: .highlighted)
+ button.setTitleColor(UIColor.gray, for: .disabled)
+ button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
+ button.addTarget(self,
+ action: #selector(onRequestUserNotificationsButtonTapped),
+ for: .touchUpInside)
+ return button
+ }()
+
+ lazy var apnsRequestPermissionsTableCell: UITableViewCell = {
+ let cell = UITableViewCell(style: .default,
+ reuseIdentifier: Row.requestAPNSPermissions.rawValue)
+ cell.selectionStyle = .none
+ cell.contentView.addSubview(self.requestPermissionsButton)
+ self.requestPermissionsButton.frame = cell.contentView.bounds
+ self.requestPermissionsButton.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ return cell
+ }()
+
+ lazy var fcmTokenTableCell: UITableViewCell = {
+ let cell = UITableViewCell(style: .subtitle, reuseIdentifier: Row.fcmToken.rawValue)
+ cell.textLabel?.numberOfLines = 0
+ cell.textLabel?.lineBreakMode = .byCharWrapping
+ return cell
+ }()
+}
+
+// MARK: - Configuring the table view and cells with information
+extension MessagingViewController {
+ func resetTableContents() {
+ sections.removeAll()
+ sectionHeaderTitles.removeAll()
+
+ // APNS
+ let apnsSection: [Row] = [.apnsToken, .apnsStatus, .requestAPNSPermissions]
+ sections.append(apnsSection)
+ sectionHeaderTitles.append("APNs")
+
+ // FCM
+ let fcmSection: [Row] = [.fcmToken]
+ sections.append(fcmSection)
+ sectionHeaderTitles.append("FCM Token")
+
+ }
+
+ func indexPathFor(_ rowId: Row) -> IndexPath? {
+ var sectionIndex = 0
+ for section in sections {
+ var rowIndex = 0
+ for row in section {
+ if row == rowId {
+ return IndexPath(row: rowIndex, section: sectionIndex)
+ }
+ rowIndex += 1
+ }
+ sectionIndex += 1
+ }
+ return nil
+ }
+
+ func configureCell(_ cell: UITableViewCell, withAPNSToken apnsToken: Data?) {
+ guard !Environment.isSimulator else {
+ cell.textLabel?.text = "APNs notifications are not supported in the simulator."
+ cell.detailTextLabel?.text = nil
+ return
+ }
+ if let apnsToken = apnsToken {
+ cell.textLabel?.text = apnsToken.hexByteString
+ cell.detailTextLabel?.text = "Tap to Share"
+ } else {
+ cell.textLabel?.text = "None"
+ cell.detailTextLabel?.text = nil
+ }
+ }
+
+ func configureCellWithAPNSStatus(_ cell: UITableViewCell) {
+ if let allowedNotificationTypes = allowedNotificationTypes {
+ let displayableTypes: [String] = allowedNotificationTypes.map { return $0.rawValue }
+ cell.detailTextLabel?.text = displayableTypes.joined(separator: ", ")
+ } else {
+ cell.detailTextLabel?.text = "Retrieving..."
+ }
+ }
+
+ func configureCell(_ cell: UITableViewCell, withFCMToken fcmToken: String?) {
+ if let fcmToken = fcmToken {
+ cell.textLabel?.text = fcmToken
+ cell.detailTextLabel?.text = "Tap to Share"
+ } else {
+ cell.textLabel?.text = "None"
+ cell.detailTextLabel?.text = nil
+ }
+ }
+}
+
+// MARK: - UITableViewDataSource
+extension MessagingViewController: UITableViewDataSource {
+ func numberOfSections(in tableView: UITableView) -> Int {
+ return sections.count
+ }
+
+ public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return sections[section].count
+ }
+
+ public func tableView(_ tableView: UITableView,
+ cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let row = sections[indexPath.section][indexPath.row]
+
+ let cell: UITableViewCell
+ switch row {
+ case .apnsToken:
+ cell = apnsTableCell
+ configureCell(cell, withAPNSToken: Messaging.messaging().apnsToken)
+ case .apnsStatus:
+ cell = apnsStatusTableCell
+ configureCellWithAPNSStatus(cell)
+ case .requestAPNSPermissions:
+ cell = apnsRequestPermissionsTableCell
+ case .fcmToken:
+ cell = fcmTokenTableCell
+ configureCell(cell, withFCMToken: Messaging.messaging().fcmToken)
+ }
+ return cell
+ }
+
+ func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ return sectionHeaderTitles[section]
+ }
+}
+
+// MARK: - UITableViewDelegate
+extension MessagingViewController: UITableViewDelegate {
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ tableView.deselectRow(at: indexPath, animated: true)
+
+ let row = sections[indexPath.section][indexPath.row]
+ switch row {
+ case .apnsToken:
+ if let apnsToken = Messaging.messaging().apnsToken {
+ showActivityViewControllerFor(sharedItem: apnsToken.hexByteString)
+ }
+ case .fcmToken:
+ if let fcmToken = Messaging.messaging().fcmToken {
+ showActivityViewControllerFor(sharedItem: fcmToken)
+ }
+ default: break
+ }
+ }
+}
+
+// MARK: - UI Controls
+extension MessagingViewController {
+ func onRequestUserNotificationsButtonTapped(sender: UIButton) {
+ NotificationsController.shared.registerForUserFacingNotificationsFor(UIApplication.shared)
+ }
+}
+
+// MARK: - Activity View Controller
+extension MessagingViewController {
+ func showActivityViewControllerFor(sharedItem: Any) {
+ let activityViewController = UIActivityViewController(activityItems: [sharedItem],
+ applicationActivities: nil)
+ present(activityViewController, animated: true, completion: nil)
+ }
+}
+
diff --git a/Example/Messaging/App/Messaging_Example.entitlements b/Example/Messaging/App/Messaging_Example.entitlements
new file mode 100644
index 0000000..903def2
--- /dev/null
+++ b/Example/Messaging/App/Messaging_Example.entitlements
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>aps-environment</key>
+ <string>development</string>
+</dict>
+</plist>
diff --git a/Example/Messaging/App/NotificationsController.swift b/Example/Messaging/App/NotificationsController.swift
new file mode 100644
index 0000000..726d980
--- /dev/null
+++ b/Example/Messaging/App/NotificationsController.swift
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import UIKit
+import UserNotifications
+
+import FirebaseDev
+
+enum NotificationsControllerAllowedNotificationType: String {
+ case none = "None"
+ case silent = "Silent Updates"
+ case alert = "Alerts"
+ case badge = "Badges"
+ case sound = "Sounds"
+}
+
+let APNSTokenReceivedNotification: Notification.Name
+ = Notification.Name(rawValue: "APNSTokenReceivedNotification")
+let UserNotificationsChangedNotification: Notification.Name
+ = Notification.Name(rawValue: "UserNotificationsChangedNotification")
+
+class NotificationsController: NSObject {
+
+ static let shared: NotificationsController = {
+ let instance = NotificationsController()
+ return instance
+ }()
+
+ class func configure() {
+ let sharedController = NotificationsController.shared
+ // Always become the delegate of UNUserNotificationCenter, even before we've requested user
+ // permissions
+ if #available(iOS 10.0, *) {
+ UNUserNotificationCenter.current().delegate = sharedController
+ }
+ }
+
+ func registerForUserFacingNotificationsFor(_ application: UIApplication) {
+ if #available(iOS 10.0, *) {
+ UNUserNotificationCenter.current()
+ .requestAuthorization(options: [.alert, .badge, .sound],
+ completionHandler: { (granted, error) in
+ NotificationCenter.default.post(name: UserNotificationsChangedNotification, object: nil)
+ })
+ } else if #available(iOS 8.0, *) {
+ let userNotificationSettings = UIUserNotificationSettings(types: [.alert, .badge, .sound],
+ categories: [])
+ application.registerUserNotificationSettings(userNotificationSettings)
+
+ } else {
+ application.registerForRemoteNotifications(matching: [.alert, .badge, .sound])
+ }
+ }
+
+ func getAllowedNotificationTypes(_ completion:
+ @escaping (_ allowedTypes: [NotificationsControllerAllowedNotificationType]) -> Void) {
+
+ guard Messaging.messaging().apnsToken != nil else {
+ completion([.none])
+ return
+ }
+
+ var types: [NotificationsControllerAllowedNotificationType] = [.silent]
+ if #available(iOS 10.0, *) {
+ UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { (settings) in
+ if settings.alertSetting == .enabled {
+ types.append(.alert)
+ }
+ if settings.badgeSetting == .enabled {
+ types.append(.badge)
+ }
+ if settings.soundSetting == .enabled {
+ types.append(.sound)
+ }
+ DispatchQueue.main.async {
+ completion(types)
+ }
+ })
+ } else if #available(iOS 8.0, *) {
+ if let userNotificationSettings = UIApplication.shared.currentUserNotificationSettings {
+ if userNotificationSettings.types.contains(.alert) {
+ types.append(.alert)
+ }
+ if userNotificationSettings.types.contains(.badge) {
+ types.append(.badge)
+ }
+ if userNotificationSettings.types.contains(.sound) {
+ types.append(.sound)
+ }
+ }
+ completion(types)
+ } else {
+ let enabledTypes = UIApplication.shared.enabledRemoteNotificationTypes()
+ if enabledTypes.contains(.alert) {
+ types.append(.alert)
+ }
+ if enabledTypes.contains(.badge) {
+ types.append(.badge)
+ }
+ if enabledTypes.contains(.sound) {
+ types.append(.sound)
+ }
+ completion(types)
+ }
+ }
+}
+
+// MARK: - UNUserNotificationCenterDelegate
+@available(iOS 10.0, *)
+extension NotificationsController: UNUserNotificationCenterDelegate {
+
+ func userNotificationCenter(_ center: UNUserNotificationCenter,
+ willPresent notification: UNNotification,
+ withCompletionHandler completionHandler:
+ @escaping (UNNotificationPresentationOptions) -> Void) {
+ // Always show the incoming notification, even if the app is in foreground
+ completionHandler([.alert, .badge, .sound])
+ }
+}