diff options
Diffstat (limited to 'Example/Messaging/App')
-rw-r--r-- | Example/Messaging/App/AppDelegate.swift | 114 | ||||
-rw-r--r-- | Example/Messaging/App/Base.lproj/LaunchScreen.storyboard | 27 | ||||
-rw-r--r-- | Example/Messaging/App/Base.lproj/Main.storyboard | 48 | ||||
-rw-r--r-- | Example/Messaging/App/Data+MessagingExtensions.swift | 25 | ||||
-rw-r--r-- | Example/Messaging/App/Environment.swift | 28 | ||||
-rw-r--r-- | Example/Messaging/App/GoogleService-Info.plist | 30 | ||||
-rw-r--r-- | Example/Messaging/App/Messaging-Info.plist | 53 | ||||
-rw-r--r-- | Example/Messaging/App/MessagingViewController.swift | 332 | ||||
-rw-r--r-- | Example/Messaging/App/Messaging_Example.entitlements | 8 | ||||
-rw-r--r-- | Example/Messaging/App/NotificationsController.swift | 132 |
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]) + } +} |