Commit d4c1ea62 authored by Jü's avatar

Merge branch 'feature/apple_watch_2fa'

* feature/apple_watch_2fa:
  fixed icon size warnings
  send QRC of current wallet to watch
  added configuration for Apple Watch 2FA
  added apple watch app for 2FA
parents 32c372e3 63697ef7
//
// ApplicationContext.swift
// XWallet
//
// Created by loj on 29.07.18.
//
import Foundation
public enum ApplicationContextTag: String {
case requestId = "requestId"
case qrcImage = "qrcImage"
}
{
"images" : [
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : "<=145"
},
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : ">145"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
{
"assets" : [
{
"idiom" : "watch",
"filename" : "Circular.imageset",
"role" : "circular"
},
{
"idiom" : "watch",
"filename" : "Extra Large.imageset",
"role" : "extra-large"
},
{
"idiom" : "watch",
"filename" : "Modular.imageset",
"role" : "modular"
},
{
"idiom" : "watch",
"filename" : "Utilitarian.imageset",
"role" : "utilitarian"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
{
"images" : [
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : "<=145"
},
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : ">145"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
{
"images" : [
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : "<=145"
},
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : ">145"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
{
"images" : [
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : "<=145"
},
{
"idiom" : "watch",
"scale" : "2x",
"screen-width" : ">145"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
//
// ExtensionDelegate.swift
// XWallet Watchkit App Extension
//
// Created by loj on 15.07.18.
//
import WatchKit
class ExtensionDelegate: NSObject, WKExtensionDelegate {
private var notificationService: NotificationServiceProtocol!
private var communicationService: CommunicationServiceProtocol!
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
self.setup()
}
func applicationDidBecomeActive() {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillResignActive() {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, etc.
}
private func setup() {
self.notificationService = NotificationService()
self.communicationService = CommunicationServiceProvider.current()
self.notificationService.register(callbackForRequestId: { (requestId: String) in
self.handleNotification(requestId: requestId)
})
self.communicationService.register(receiveRequestIdHandler: { (requestId: String) in
self.handleCommunication(requestId: requestId)
})
self.communicationService.register(receiveQrcHandler: { (qrcImage: UIImage) in
self.handleCommunication(qrcImage: qrcImage)
})
}
private func handleNotification(requestId: String) {
self.communicationService.authenticate(requestId: requestId)
}
private func handleCommunication(requestId: String) {
if WKExtension.shared().applicationState == .background {
print("*** watch: is in background")
self.notificationService.schedule(requestId: requestId)
} else {
print("*** watch: is in foreground")
let context = [ApplicationContextTag.requestId.rawValue:requestId]
DispatchQueue.main.async {
WKInterfaceController.reloadRootPageControllers(
withNames: ["MainView"],
contexts: [context],
orientation: WKPageOrientation.vertical,
pageIndex: 0)
}
}
}
private func handleCommunication(qrcImage: UIImage) {
if WKExtension.shared().applicationState == .background {
print("*** watch: is in background")
// self.notificationService.schedule(requestId: requestId)
} else {
print("*** watch: is in foreground")
let context = [ApplicationContextTag.qrcImage.rawValue:qrcImage]
DispatchQueue.main.async {
WKInterfaceController.reloadRootPageControllers(
withNames: ["MainView"],
contexts: [context],
orientation: WKPageOrientation.vertical,
pageIndex: 0)
}
}
}
}
<?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>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>XWallet Watchkit App Extension</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>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.7</string>
<key>CFBundleVersion</key>
<string>1.7.0.0</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>WKAppBundleIdentifier</key>
<string>com.XMRSystemsLLC.XWallet.watchkitapp</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.watchkit</string>
</dict>
<key>WKExtensionDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).ExtensionDelegate</string>
</dict>
</plist>
//
// InterfaceController.swift
// XWallet Watchkit App Extension
//
// Created by loj on 15.07.18.
//
import Foundation
import WatchKit
class InterfaceController: WKInterfaceController {
@IBOutlet weak var label: WKInterfaceLabel!
@IBOutlet weak var authenticateButton: WKInterfaceButton!
@IBOutlet weak var qrcImage: WKInterfaceImage!
@IBAction func authenticateButtonTouched() {
if let requestId = self.requestId {
self.communicationService.authenticate(requestId: requestId)
self.requestId = nil
}
self.updateControls(authenticate: false)
}
private var communicationService: CommunicationServiceProtocol!
private var requestId: String?
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// Configure interface objects here.
print("*** watch: interfaceController.awake")
self.communicationService = CommunicationServiceProvider.current()
self.handle(context)
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
print("*** watch: interfaceController.willActivate")
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
print("*** watch: interfaceController.didDeactivate")
}
private func updateControls(authenticate: Bool) {
if authenticate {
self.showAuthenticateButton()
} else {
self.showQRC()
}
}
private func handle(_ context: Any?) {
guard let dictionary = context as? [String:Any?] else {
return
}
if let requestId = dictionary[ApplicationContextTag.requestId.rawValue] as? String {
self.requestId = requestId
let askForAuthentication = self.requestId != nil
self.updateControls(authenticate: askForAuthentication)
}
if let qrcImage = dictionary[ApplicationContextTag.qrcImage.rawValue] as? UIImage {
self.qrcImage.setImage(qrcImage)
}
}
private func showQRC() {
self.qrcImage.setHidden(false)
self.label.setHidden(true)
self.authenticateButton.setHidden(true)
}
private func showAuthenticateButton() {
self.label.setHidden(false)
self.authenticateButton.setHidden(false)
self.qrcImage.setHidden(true)
}
}
//
// NotificationController.swift
// XWallet Watchkit App Extension
//
// Created by loj on 15.07.18.
//
import WatchKit
import Foundation
import UserNotifications
class NotificationController: WKUserNotificationInterfaceController {
override init() {
// Initialize variables here.
super.init()
// Configure interface objects here.
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification,
withCompletion completionHandler: @escaping (WKUserNotificationInterfaceType) -> Swift.Void) {
// This method is called when a notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
//
// After populating your dynamic notification interface call the completion block.
completionHandler(.custom)
}
}
{
"aps": {
"alert": {
"body": "Test message",
"title": "Optional title"
},
"category": "myCategory",
"thread-id":"5280"
},
"WatchKit Simulator Actions": [
{
"title": "First Button",
"identifier": "firstButtonAction"
}
],
"customKey": "Use this file to define a testing payload for your notifications. The aps dictionary specifies the category, alert text and title. The WatchKit Simulator Actions array can provide info for one or more action buttons in addition to the standard Dismiss button. Any other top level keys are custom payload. If you have multiple such JSON files in your project, you'll be able to select them when choosing to debug the notification interface of your Watch App."
}
//
// CommunicationService.swift
// XWallet Watchkit App Extension
//
// Created by loj on 29.07.18.
//
import Foundation
import WatchConnectivity
import WatchKit
public protocol CommunicationServiceProtocol {
func register(receiveQrcHandler: @escaping (_ qrcImage: UIImage) -> Void)
func register(receiveRequestIdHandler: @escaping (_ requestId: String) -> Void)
func authenticate(requestId: String)
}
public class CommunicationService: NSObject, CommunicationServiceProtocol {
private var receiveRequestIdHandler: ((String) -> Void)?
private var receiveQrcHandler: ((UIImage) -> Void)?
public override init() {
super.init()
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
}
}
public func register(receiveQrcHandler: @escaping (UIImage) -> Void) {
self.receiveQrcHandler = receiveQrcHandler
}
public func register(receiveRequestIdHandler: @escaping (_ requestId: String) -> Void) {
self.receiveRequestIdHandler = receiveRequestIdHandler
}
public func authenticate(requestId: String) {
if WCSession.isSupported() {
let message = [ApplicationContextTag.requestId.rawValue:"\(requestId)"]
let session = WCSession.default
do {
print("*** watch: sending to phone: \(message)")
try session.updateApplicationContext(message)
} catch {
print("*** watch: sending to phone failed: \(error)")
}
}
}
}
extension CommunicationService: WCSessionDelegate {
public func session(_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?)
{
if let error = error {
print("*** watch: WC Session activation failed with error: \(error.localizedDescription)")
return
}
print("*** watch: WC Session activated successfully with state: \(activationState.rawValue)")
}
public func session(_ session: WCSession,
didReceiveApplicationContext applicationContext: [String : Any])
{
//print("*** watch: recieved applicationContext: \(applicationContext)")
if let requestId = applicationContext[ApplicationContextTag.requestId.rawValue] as? String {
print("*** watch: received a request id")
self.receiveRequestIdHandler?(requestId)
return
}
if let imageData = applicationContext[ApplicationContextTag.qrcImage.rawValue] as? Data {
print("*** watch: received an image")
guard let qrcImage = UIImage(data: imageData) else {
print("*** watch: received invalid image data, exiting")
return
}
self.receiveQrcHandler?(qrcImage)
return
}
print("*** watch: recieved unknown tag, exiting")
}
}
//
// CommunicationServiceProvider.swift
// XWallet Watchkit App Extension
//
// Created by loj on 30.07.18.
//
import Foundation
public protocol CommunicationServiceProviderProtocol {
static func current() -> CommunicationServiceProtocol
}
public class CommunicationServiceProvider: CommunicationServiceProviderProtocol {
public static func current() -> CommunicationServiceProtocol {
return communicationService
}
private static var communicationService = {
return CommunicationService()
}()
}
//
// NotificationService.swift
// XWallet Watchkit App Extension
//
// Created by loj on 29.07.18.
//
import Foundation
import UserNotifications
public protocol NotificationServiceProtocol {
func register(callbackForRequestId: @escaping (_ requestId: String) -> Void)
func schedule(requestId: String)
}
public class NotificationService: NSObject, NotificationServiceProtocol {
private let authenticateActionTag = "authenticateAction"
private var callbackForRequestId: (String) -> Void = { _ in }
public override init() {
super.init()
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { (granted, error) in
if granted {
let authenticateAction = UNNotificationAction(identifier: self.authenticateActionTag,
title: "Authenticate",
options: .foreground)
let xwalletCategory = UNNotificationCategory(identifier: "XWalletCategory",
actions: [authenticateAction],
intentIdentifiers: [],
options: [])
UNUserNotificationCenter.current().setNotificationCategories([xwalletCategory])
UNUserNotificationCenter.current().delegate = self
print("*** watch: successfully registered notification support")
} else {
print("*** watch: register notification support failed: \(String(describing: error?.localizedDescription))")
}
}
}
public func register(callbackForRequestId: @escaping (String) -> Void) {
self.callbackForRequestId = callbackForRequestId
}
public func schedule(requestId: String) {
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
if settings.alertSetting != .enabled {
print("*** watch: notification alerts are disabled")
return
}
self.removeAllOtherPendingNotifications()
let notificationRequest = self.buildNotificationRequest(with: requestId)
UNUserNotificationCenter.current().add(notificationRequest) { (error) in
if let error = error {
print("*** watch: adding notification failed:\(error.localizedDescription)")
} else {
print("*** watch: local notification was scheduled")
}
}
}
}
private func removeAllOtherPendingNotifications() {
UNUserNotificationCenter.current().getDeliveredNotifications { (notifications: [UNNotification]) in
let identifiers = notifications.map { $0.request.identifier }
print("got identifiers: \(identifiers)")
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
}
}
private func buildNotificationRequest(with requestId: String) -> UNNotificationRequest {
let notificationContent = UNMutableNotificationContent()
notificationContent.body = "Allow access?"
notificationContent.categoryIdentifier = "XWalletCategory"
notificationContent.userInfo = [ApplicationContextTag.requestId.rawValue:requestId]
let notificationRequest = UNNotificationRequest(identifier: UUID().uuidString,
content: notificationContent,
trigger: nil)
return notificationRequest
}
}
extension NotificationService: UNUserNotificationCenterDelegate {
public func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void)
{
print("*** watch: did receive notification, actionIdentifier: \(response.actionIdentifier)")
if response.actionIdentifier == self.authenticateActionTag {
if let responseId = response.notification.request.content.userInfo[ApplicationContextTag.requestId.rawValue] as? String {
self.callbackForRequestId(responseId)
return
} else {
print("*** watch: unable to get responseId from userInfo: \(response.notification.request.content.userInfo)")
}
} else {
print("*** watch: this actionIdentifier is not handled: \(response.actionIdentifier)")
}
completionHandler()
}
}