From 41bf25b72fa00047f911f8c458df1ac6fae619ee Mon Sep 17 00:00:00 2001 From: Jan Kobersky <5406945+kober32@users.noreply.github.com> Date: Wed, 27 Nov 2024 08:53:09 +0100 Subject: [PATCH] Push Notifications improvements (#178) --- .github/workflows/tests.yml | 3 +- .../project.pbxproj | 28 +++ .../ProvisioningUtils/WMTMachOReader.swift | 185 ++++++++++++++++++ .../WMTProvisioningUtils.swift | 137 +++++++++++++ .../WMTSignatureAPNSEnvironmentDetector.swift | 46 +++++ WultraMobileTokenSDK/Common/WMTUtils.swift | 33 ++++ .../Push/Model/WMTPushRegistrationData.swift | 33 +++- .../Push/Service/WMTPushImpl.swift | 70 +++++-- WultraMobileTokenSDK/Push/WMTPush.swift | 43 ++++ WultraMobileTokenSDKTests/Configs/Readme.md | 1 + .../IntegrationProxy.swift | 13 +- .../IntegrationTests.swift | 96 +++++---- .../NetworkingObjectsTests.swift | 113 ++++++++--- .../ProvisioningUtilsTests.swift | 70 +++++++ docs/Changelog.md | 3 +- docs/Using-Push-Service.md | 41 +++- scripts/test.sh | 7 + 17 files changed, 823 insertions(+), 99 deletions(-) create mode 100644 WultraMobileTokenSDK/Common/ProvisioningUtils/WMTMachOReader.swift create mode 100644 WultraMobileTokenSDK/Common/ProvisioningUtils/WMTProvisioningUtils.swift create mode 100644 WultraMobileTokenSDK/Common/ProvisioningUtils/WMTSignatureAPNSEnvironmentDetector.swift create mode 100644 WultraMobileTokenSDK/Common/WMTUtils.swift create mode 100644 WultraMobileTokenSDKTests/ProvisioningUtilsTests.swift diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c81f9fe..1ae878d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,4 +28,5 @@ jobs: OP_URL: ${{ secrets.TESTS_OP_URL }} ER_URL: ${{ secrets.TESTS_ER_URL }} IN_URL: ${{ secrets.TESTS_IN_URL }} - run: ./scripts/test.sh -destination "platform=iOS Simulator,OS=17.0.1,name=iPhone 15" -sdkconfig "$SDK_CONFIG" -er "$ER_URL" -op "$OP_URL" -in "$IN_URL" -cl "$CL_URL" -clu "$CL_LGN" -clp "$CL_PWD" -cla "$CL_AID" \ No newline at end of file + PU_URL: ${{ secrets.TESTS_PU_URL }} + run: ./scripts/test.sh -destination "platform=iOS Simulator,OS=17.0.1,name=iPhone 15" -sdkconfig "$SDK_CONFIG" -er "$ER_URL" -op "$OP_URL" -in "$IN_URL" -cl "$CL_URL" -clu "$CL_LGN" -clp "$CL_PWD" -cla "$CL_AID" -pu "$PU_URL" \ No newline at end of file diff --git a/WultraMobileTokenSDK.xcodeproj/project.pbxproj b/WultraMobileTokenSDK.xcodeproj/project.pbxproj index d78a9f3..e8b2f11 100644 --- a/WultraMobileTokenSDK.xcodeproj/project.pbxproj +++ b/WultraMobileTokenSDK.xcodeproj/project.pbxproj @@ -44,6 +44,9 @@ DCA43C6D2993F63E0059A163 /* WMTOperationAttributeImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA43C6C2993F63E0059A163 /* WMTOperationAttributeImage.swift */; }; DCAB7BC824580B4C0006989D /* WMTQROperationParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAB7BC724580B4C0006989D /* WMTQROperationParser.swift */; }; DCAB7BCA24580BAC0006989D /* WMTQROperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAB7BC924580BAC0006989D /* WMTQROperation.swift */; }; + DCAC55992CE68C2A0070644A /* ProvisioningUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC55982CE68C2A0070644A /* ProvisioningUtilsTests.swift */; }; + DCAC559C2CE773E90070644A /* WMTProvisioningUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC559B2CE773E90070644A /* WMTProvisioningUtils.swift */; }; + DCAC55BC2CEC954C0070644A /* WMTUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC55BB2CEC954C0070644A /* WMTUtils.swift */; }; DCC3420424E3DB310045D27D /* WMTPushParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC3420324E3DB310045D27D /* WMTPushParser.swift */; }; DCC5CC9F2449EE21004679AC /* MobileTokenSDK.h in Headers */ = {isa = PBXBuildFile; fileRef = DCC5CC9D2449EE21004679AC /* MobileTokenSDK.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCC5CCAC2449F765004679AC /* WMTOperationsImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC5CCAB2449F765004679AC /* WMTOperationsImpl.swift */; }; @@ -63,6 +66,8 @@ DCD8B336246C1BAF00385F02 /* WMTRejectionReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD8B335246C1BAF00385F02 /* WMTRejectionReason.swift */; }; DCE660D124CEBECA00870E53 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE660D024CEBECA00870E53 /* IntegrationTests.swift */; }; DCE660D324CEF56400870E53 /* IntegrationProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE660D224CEF56400870E53 /* IntegrationProxy.swift */; }; + DCE6D5742CF5F46000865D6E /* WMTSignatureAPNSEnvironmentDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D5732CF5F46000865D6E /* WMTSignatureAPNSEnvironmentDetector.swift */; }; + DCE6D5772CF5F5D500865D6E /* WMTMachOReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE6D5762CF5F5D500865D6E /* WMTMachOReader.swift */; }; EA294F3D29F6A07A00A0494E /* WMTOperationUIData.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA294F3C29F6A07A00A0494E /* WMTOperationUIData.swift */; }; EA44366A29F9294600DDEC1C /* WMTPostApprovaScreenReview.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA44366929F9294600DDEC1C /* WMTPostApprovaScreenReview.swift */; }; EA44366C29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA44366B29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift */; }; @@ -126,6 +131,9 @@ DCA43C6C2993F63E0059A163 /* WMTOperationAttributeImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationAttributeImage.swift; sourceTree = ""; }; DCAB7BC724580B4C0006989D /* WMTQROperationParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTQROperationParser.swift; sourceTree = ""; }; DCAB7BC924580BAC0006989D /* WMTQROperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTQROperation.swift; sourceTree = ""; }; + DCAC55982CE68C2A0070644A /* ProvisioningUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProvisioningUtilsTests.swift; sourceTree = ""; }; + DCAC559B2CE773E90070644A /* WMTProvisioningUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTProvisioningUtils.swift; sourceTree = ""; }; + DCAC55BB2CEC954C0070644A /* WMTUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTUtils.swift; sourceTree = ""; }; DCC3420324E3DB310045D27D /* WMTPushParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPushParser.swift; sourceTree = ""; }; DCC5CC9A2449EE21004679AC /* WultraMobileTokenSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WultraMobileTokenSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DCC5CC9D2449EE21004679AC /* MobileTokenSDK.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MobileTokenSDK.h; sourceTree = ""; }; @@ -150,6 +158,8 @@ DCD8B335246C1BAF00385F02 /* WMTRejectionReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTRejectionReason.swift; sourceTree = ""; }; DCE660D024CEBECA00870E53 /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; DCE660D224CEF56400870E53 /* IntegrationProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationProxy.swift; sourceTree = ""; }; + DCE6D5732CF5F46000865D6E /* WMTSignatureAPNSEnvironmentDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTSignatureAPNSEnvironmentDetector.swift; sourceTree = ""; }; + DCE6D5762CF5F5D500865D6E /* WMTMachOReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTMachOReader.swift; sourceTree = ""; }; EA294F3C29F6A07A00A0494E /* WMTOperationUIData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationUIData.swift; sourceTree = ""; }; EA44366929F9294600DDEC1C /* WMTPostApprovaScreenReview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovaScreenReview.swift; sourceTree = ""; }; EA44366B29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovaScreenRedirect.swift; sourceTree = ""; }; @@ -236,6 +246,16 @@ path = UserOperation; sourceTree = ""; }; + DC3E529E2CF62891002621C1 /* ProvisioningUtils */ = { + isa = PBXGroup; + children = ( + DCAC559B2CE773E90070644A /* WMTProvisioningUtils.swift */, + DCE6D5732CF5F46000865D6E /* WMTSignatureAPNSEnvironmentDetector.swift */, + DCE6D5762CF5F5D500865D6E /* WMTMachOReader.swift */, + ); + path = ProvisioningUtils; + sourceTree = ""; + }; DC488034292282FF00DB844B /* Inbox */ = { isa = PBXGroup; children = ( @@ -277,6 +297,7 @@ DCE660D024CEBECA00870E53 /* IntegrationTests.swift */, DCE660D224CEF56400870E53 /* IntegrationProxy.swift */, DC395C0924E55B9B0007C36E /* PushParserTests.swift */, + DCAC55982CE68C2A0070644A /* ProvisioningUtilsTests.swift */, DC6EDB7825A49ED900A229E4 /* OperationExpirationTests.swift */, DC616235248508F8000DED17 /* QROperationParserTests.swift */, EAB705492AF1161500756AC2 /* PACParserTests.swift */, @@ -356,11 +377,13 @@ DC81D1CE24502E0300F80CD6 /* Common */ = { isa = PBXGroup; children = ( + DC3E529E2CF62891002621C1 /* ProvisioningUtils */, BFEEB20A2937AD700047941D /* WMTCancellable.swift */, DC488030292282C900DB844B /* WMTService.swift */, DCC5CCCD244DB0AD004679AC /* WMTLogger.swift */, DC06D01E25AC74E400F2EA69 /* WMTLock.swift */, DC9511F826EA02C100FF40AD /* WPNIntegration.swift */, + DCAC55BB2CEC954C0070644A /* WMTUtils.swift */, ); path = Common; sourceTree = ""; @@ -588,6 +611,7 @@ files = ( DC61624224852B6D000DED17 /* NetworkingObjectsTests.swift in Sources */, DC395C0A24E55B9B0007C36E /* PushParserTests.swift in Sources */, + DCAC55992CE68C2A0070644A /* ProvisioningUtilsTests.swift in Sources */, DC6EDB7925A49ED900A229E4 /* OperationExpirationTests.swift in Sources */, EAB7054A2AF1161500756AC2 /* PACParserTests.swift in Sources */, DC616236248508F8000DED17 /* QROperationParserTests.swift in Sources */, @@ -610,6 +634,7 @@ DC3D0B392480F886000DC4D9 /* WMTLocalOperation.swift in Sources */, DCD8B336246C1BAF00385F02 /* WMTRejectionReason.swift in Sources */, DCC5CCD8244DBBBD004679AC /* WMTAuthorizationData.swift in Sources */, + DCAC55BC2CEC954C0070644A /* WMTUtils.swift in Sources */, DC488040292282FF00DB844B /* WMTInboxCount.swift in Sources */, DCA43C6B29927C960059A163 /* WMTOperationAttributeAmountConversion.swift in Sources */, EA74F7B32C2561BB004340B9 /* WMTResultTexts.swift in Sources */, @@ -639,12 +664,15 @@ DCC5CCCE244DB0AD004679AC /* WMTLogger.swift in Sources */, DCC5CCAE2449F7AC004679AC /* WMTUserOperation.swift in Sources */, DC9511F926EA02C100FF40AD /* WPNIntegration.swift in Sources */, + DCE6D5772CF5F5D500865D6E /* WMTMachOReader.swift in Sources */, DCC5CCBD2449F965004679AC /* WMTOperationAttributeHeading.swift in Sources */, + DCAC559C2CE773E90070644A /* WMTProvisioningUtils.swift in Sources */, DC8CB206244DD007009DDAA3 /* WMTAllowedOperationSignature.swift in Sources */, DCC3420424E3DB310045D27D /* WMTPushParser.swift in Sources */, BFEEB20529379C700047941D /* WMTInboxGetMessageDetail.swift in Sources */, EACAF7B02A126B7D0021CA54 /* WMTJsonValue.swift in Sources */, DCAB7BCA24580BAC0006989D /* WMTQROperation.swift in Sources */, + DCE6D5742CF5F46000865D6E /* WMTSignatureAPNSEnvironmentDetector.swift in Sources */, DCC5CCBF2449F981004679AC /* WMTOperationAttributePartyInfo.swift in Sources */, DC81D1CB244F451E00F80CD6 /* WMTPushImpl.swift in Sources */, EA9CE2BE2AEAA9FD00FE4E35 /* WMTProximityCheck.swift in Sources */, diff --git a/WultraMobileTokenSDK/Common/ProvisioningUtils/WMTMachOReader.swift b/WultraMobileTokenSDK/Common/ProvisioningUtils/WMTMachOReader.swift new file mode 100644 index 0000000..35f92ef --- /dev/null +++ b/WultraMobileTokenSDK/Common/ProvisioningUtils/WMTMachOReader.swift @@ -0,0 +1,185 @@ +// +// Copyright 2024 Wultra s.r.o. +// +// 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 +import MachO +import CommonCrypto + +class WMTMachOReader { + + private struct CSSuperBlob { + var magic: UInt32 + var length: UInt32 + var count: UInt32 + } + + private struct CSBlob { + var type: UInt32 + var offset: UInt32 + } + + private struct CSMagic { + static let embeddedSignature: UInt32 = 0xfade0cc0 + static let embeddedEntitlements: UInt32 = 0xfade7171 + } + + private enum BinaryType { + struct HeaderData { + let headerSize: Int + let commandCount: Int + } + struct FatHeaderData { + let archCount: Int + } + case singleArch(headerInfo: HeaderData) + case fat(header: FatHeaderData) + } + + private var entitlements = [WMTProvision.Entitlements]() + + static func readEntitlements(_ binaryPath: String) -> [WMTProvision.Entitlements]? { + WMTMachOReader(binaryPath)?.entitlements + } + + private init?(_ binaryPath: String) { + guard let binary = BinaryReader(binaryPath) else { + return nil + } + + switch getBinaryType(binary: binary) { + case .singleArch(let headerInfo): + let headerSize = headerInfo.headerSize + let commandCount = headerInfo.commandCount + if let data = readEntitlementsFromBinarySlice(binary: binary, headerOffset: headerSize, dataOffset: 0, cmdCount: commandCount) { + entitlements.append(data) + } + case .fat(let header): + entitlements.append(contentsOf: readEntitlementsFromFatBinary(binary: binary, architectureCount: header.archCount, startingAt: MemoryLayout.size)) + default: + return nil + } + } + + private func getBinaryType(binary: BinaryReader, fromSliceStartingAt offset: UInt64 = 0) -> BinaryType? { + binary.seek(to: offset) + let header: mach_header = binary.read() + let commandCount = Int(header.ncmds) + switch header.magic { + case MH_MAGIC: + let data = BinaryType.HeaderData(headerSize: MemoryLayout.size, commandCount: commandCount) + return .singleArch(headerInfo: data) + case MH_MAGIC_64: + let data = BinaryType.HeaderData(headerSize: MemoryLayout.size, commandCount: commandCount) + return .singleArch(headerInfo: data) + default: + binary.seek(to: 0) + let fatHeader: fat_header = binary.read() + if CFSwapInt32(fatHeader.magic) == FAT_MAGIC { + let archCount = Int(CFSwapInt32(fatHeader.nfat_arch)) + return .fat(header: BinaryType.FatHeaderData(archCount: archCount)) + } else { + return nil + } + } + } + + private func readEntitlementsFromFatBinary(binary: BinaryReader, architectureCount: Int, startingAt: Int) -> [WMTProvision.Entitlements] { + var entitlements = [WMTProvision.Entitlements]() + for i in 0...size) + binary.seek(to: UInt64(offset)) + let fatArch: fat_arch = binary.read() + let fatArchOffset = CFSwapInt32(fatArch.offset) + let arch = getBinaryType(binary: binary, fromSliceStartingAt: UInt64(fatArchOffset)) + switch arch { + case .singleArch(let headerInfo): + let headerOffset = Int(fatArchOffset) + headerInfo.headerSize + if let parsed = readEntitlementsFromBinarySlice(binary: binary, headerOffset: headerOffset, dataOffset: fatArchOffset, cmdCount: headerInfo.commandCount) { + entitlements.append(parsed) + } + default: + break + } + } + return entitlements + } + + private func readEntitlementsFromBinarySlice(binary: BinaryReader, headerOffset: Int, dataOffset: UInt32, cmdCount: Int) -> WMTProvision.Entitlements? { + binary.seek(to: UInt64(headerOffset)) + for _ in 0...size))) + } + return nil + } + + private func readEntitlementsData(binary: BinaryReader, startingAt offset: UInt32) -> WMTProvision.Entitlements? { + binary.seek(to: UInt64(offset)) + let metaBlob: CSSuperBlob = binary.read() + if CFSwapInt32(metaBlob.magic) == CSMagic.embeddedSignature { + let metaBlobSize = UInt32(MemoryLayout.size) + let blobSize = UInt32(MemoryLayout.size) + let itemCount = CFSwapInt32(metaBlob.count) + for index in 0..() -> T { + handle.readData(ofLength: MemoryLayout.size).withUnsafeBytes({ $0.load(as: T.self) }) + } + + func readData(ofLength length: Int) -> Data { + handle.readData(ofLength: length) + } + + deinit { + handle.closeFile() + } +} diff --git a/WultraMobileTokenSDK/Common/ProvisioningUtils/WMTProvisioningUtils.swift b/WultraMobileTokenSDK/Common/ProvisioningUtils/WMTProvisioningUtils.swift new file mode 100644 index 0000000..84ebcd1 --- /dev/null +++ b/WultraMobileTokenSDK/Common/ProvisioningUtils/WMTProvisioningUtils.swift @@ -0,0 +1,137 @@ +// +// Copyright 2024 Wultra s.r.o. +// +// 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 + +class WMTProvisioningUtils { + + static func getMainProvisioningProfile() -> WMTProvision? { + D.debug("Retrieving embedded provisioning profile from the main bundle.") + guard let filePath = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision") else { + D.error("Missing embedded provisioning profile in the main bundle.") + return nil + } + let url = URL(fileURLWithPath: filePath) + do { + let data = try Data(contentsOf: url) + return getProvisioningProfileFromData(data) + } catch let e { + D.error("Failed to load provisioning profile: \(e)") + return nil + } + } + + static func getProvisioningProfileFromData(_ profile: Data) -> WMTProvision? { + guard let string = String(data: profile, encoding: .isoLatin1) else { + D.error("Failed to decode provisioning profile data in ISO Latin 1.") + return nil + } + let scanner = Scanner(string: string as String) + guard scanner.scanUpTo("", into: &extractedPlist) != false else { + D.error("Search for provisioning profile plist end tag failed.") + return nil + } + + guard let plist = extractedPlist?.appending("").data(using: .isoLatin1) else { + D.error("Failed to convert provisioning profile plist to data.") + return nil + } + return parseProvisioningProfilePlist(plist) + } + + static func parseProvisioningProfilePlist(_ plist: Data) -> WMTProvision? { + do { + let provision = try PropertyListDecoder().decode(WMTProvision.self, from: plist) + D.debug("Successfully parsed provisioning profile (apns env: \(provision.entitlements.apsEnvironment?.rawValue ?? "nil")).") + return provision + } catch let e { + D.error("Failed to parse provisioning profile: \(e)") + return nil + } + } +} + +/// Provisioning profile plist structure. Note that we need only entitlements for now. +/// The rest of the struct is commented out in case something changes so it does not break the parser unnecesarilly +struct WMTProvision: Decodable { + /* + var name: String + var appIDName: String + var platform: [String] + var isXcodeManaged: Bool? = false + var creationDate: Date + var expirationDate: Date + */ + var entitlements: Entitlements + + private enum CodingKeys: String, CodingKey { + /* + case name = "Name" + case appIDName = "AppIDName" + case platform = "Platform" + case isXcodeManaged = "IsXcodeManaged" + case creationDate = "CreationDate" + case expirationDate = "ExpirationDate" + */ + case entitlements = "Entitlements" + } + + struct Entitlements: Decodable { + /* + let keychainAccessGroups: [String] + let getTaskAllow: Bool + */ + let apsEnvironment: Environment? + + private enum CodingKeys: String, CodingKey { + /* + case keychainAccessGroups = "keychain-access-groups" + case getTaskAllow = "get-task-allow" + */ + case apsEnvironment = "aps-environment" + } + + enum Environment: String, Decodable { + case development + case production + } + + init(/*keychainAccessGroups: [String], getTaskAllow: Bool,*/ apsEnvironment: Environment?) { + /* + self.keychainAccessGroups = keychainAccessGroups + self.getTaskAllow = getTaskAllow + */ + self.apsEnvironment = apsEnvironment + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + /* + let keychainAccessGroups: [String] = (try? container.decode([String].self, forKey: .keychainAccessGroups)) ?? [] + let getTaskAllow: Bool = (try? container.decode(Bool.self, forKey: .getTaskAllow)) ?? false + */ + let apsEnvironment = try? container.decode(Environment.self, forKey: .apsEnvironment) + + self.init(/*keychainAccessGroups: keychainAccessGroups, getTaskAllow: getTaskAllow,*/ apsEnvironment: apsEnvironment) + } + } +} diff --git a/WultraMobileTokenSDK/Common/ProvisioningUtils/WMTSignatureAPNSEnvironmentDetector.swift b/WultraMobileTokenSDK/Common/ProvisioningUtils/WMTSignatureAPNSEnvironmentDetector.swift new file mode 100644 index 0000000..a788324 --- /dev/null +++ b/WultraMobileTokenSDK/Common/ProvisioningUtils/WMTSignatureAPNSEnvironmentDetector.swift @@ -0,0 +1,46 @@ +// +// Copyright 2024 Wultra s.r.o. +// +// 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 + +class WMTSignatureAPNSEnvironmentDetector { + + static func detectAPNSEnvironment() -> WMTProvision.Entitlements? { + + D.debug("Parsing main bundle signature to get APNS Environment.") + + guard let executableName = Bundle.main.infoDictionary?[kCFBundleExecutableKey as String] as? String else { + D.error("Could not read executable name from Info.plist") + return nil + } + guard let executablePath = Bundle.main.path(forResource: executableName, ofType: nil) else { + D.error("Could not find executable \(executableName)") + return nil + } + + guard let entitlements = WMTMachOReader.readEntitlements(executablePath) else { + D.error("Could not read entitlements from \(executablePath)") + return nil + } + + guard entitlements.isEmpty == false else { + D.info("Not entitlements found") + return nil + } + + return entitlements.first(where: { $0.apsEnvironment != nil }) + } +} diff --git a/WultraMobileTokenSDK/Common/WMTUtils.swift b/WultraMobileTokenSDK/Common/WMTUtils.swift new file mode 100644 index 0000000..e77b8f8 --- /dev/null +++ b/WultraMobileTokenSDK/Common/WMTUtils.swift @@ -0,0 +1,33 @@ +// +// Copyright 2024 Wultra s.r.o. +// +// 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 + +internal extension Data { + + static let toHexTable: [Character] = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" ] + + func toHex() -> String { + var result = "" + result.reserveCapacity(count * 2) + for byte in self { + let byteAsUInt = Int(byte) + result.append(Self.toHexTable[byteAsUInt >> 4]) + result.append(Self.toHexTable[byteAsUInt & 15]) + } + return result + } +} diff --git a/WultraMobileTokenSDK/Push/Model/WMTPushRegistrationData.swift b/WultraMobileTokenSDK/Push/Model/WMTPushRegistrationData.swift index d7a041e..cc18379 100644 --- a/WultraMobileTokenSDK/Push/Model/WMTPushRegistrationData.swift +++ b/WultraMobileTokenSDK/Push/Model/WMTPushRegistrationData.swift @@ -18,11 +18,36 @@ import Foundation class WMTPushRegistrationData: Codable { - let platform: String + let platform: WMTPushRegistrationPlatform let token: String + let environment: WMTPushRegistrationEnvironment? - init(token: String) { - self.platform = "ios" - self.token = token + init(platform: WMTPushRegistrationPlatform, token: String, environment: WMTPushRegistrationEnvironment?) { + self.platform = platform + self.token = token + self.environment = environment + } +} + +enum WMTPushRegistrationPlatform: String, Codable { + case ios // for backwards compatibility + case apns + case fcm + // case hms - Huawei Messaging Service not available for iOS +} + +enum WMTPushRegistrationEnvironment: String, Codable { + case production + case development +} + +extension WMTProvision.Entitlements.Environment { + var serverObject: WMTPushRegistrationEnvironment { + switch self { + case .development: + return .development + case .production: + return .production + } } } diff --git a/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift b/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift index 3fe5086..a460d05 100644 --- a/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift +++ b/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift @@ -62,6 +62,37 @@ class WMTPushImpl: WMTPush, WMTService { @discardableResult func registerDeviceTokenForPushNotifications(token: Data, completion: @escaping (Result) -> Void) -> Operation? { + // ios for backwards compatibility + return registerPush( + platform: .ios, + token: token.toHex(), + environment: getPushEnvironment(environment: .automatic), + completion: completion + ) + } + + @discardableResult + func register(to platform: WMTPushPlatform, completion: @escaping (Result) -> Void) -> Operation? { + + let payloadPlatform: WMTPushRegistrationPlatform + let payloadToken = platform.token + let payloadEnvironment: WMTPushRegistrationEnvironment? + + switch platform { + case .apns(_, let environment): + payloadPlatform = .apns + payloadEnvironment = getPushEnvironment(environment: environment) + case .fcm: + payloadPlatform = .fcm + payloadEnvironment = nil // no env for FCM + } + + D.info("Registering push for \(payloadPlatform.rawValue) platform.") + + return registerPush(platform: payloadPlatform, token: payloadToken, environment: payloadEnvironment, completion: completion) + } + + private func registerPush(platform: WMTPushRegistrationPlatform, token: String, environment: WMTPushRegistrationEnvironment?, completion: @escaping (Result) -> Void) -> Operation? { guard validateActivation(completion) else { return nil @@ -77,7 +108,7 @@ class WMTPushImpl: WMTPush, WMTService { pendingRegistrationForRemotePushNotifications = true pushNotificationsRegisteredOnServer = false - let data = WMTPushRegistrationData(token: HexadecimalString.encodeData(token)) + let data = WMTPushRegistrationData(platform: platform, token: token, environment: environment) return networking.post(data: .init(data), signedWith: .possession(), to: WMTPushEndpoints.RegisterDevice.endpoint) { _, error in self.pendingRegistrationForRemotePushNotifications = false @@ -90,21 +121,32 @@ class WMTPushImpl: WMTPush, WMTService { } } } + + private func getPushEnvironment(environment: WMTPushAPNSEnvironment) -> WMTPushRegistrationEnvironment? { + switch environment { + case .development: + D.info("Using APNS development environment for push notifications.") + return .development + case .production: + D.info("Using APNS production environment for push notifications.") + return .production + case .automatic: + let env = WMTProvisioningUtils.getMainProvisioningProfile()?.entitlements.apsEnvironment ?? WMTSignatureAPNSEnvironmentDetector.detectAPNSEnvironment()?.apsEnvironment + if let env { + D.info("Using \(env) environment for push notifications (automatic resolution).") + } else { + D.warning("No APNS environment found in provisioning profile. Server configuration will be used.") + } + return env?.serverObject + } + } } -private class HexadecimalString { - - static let toHexTable: [Character] = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" ] - - static func encodeData(_ data: Data) -> String { - var result = "" - result.reserveCapacity(data.count * 2) - for byte in data { - let byteAsUInt = Int(byte) - result.append(toHexTable[byteAsUInt >> 4]) - result.append(toHexTable[byteAsUInt & 15]) +extension WMTPushPlatform { + var token: String { + return switch self { + case .apns(token: let token, environment: _): token.toHex() + case .fcm(token: let token): token } - return result } - } diff --git a/WultraMobileTokenSDK/Push/WMTPush.swift b/WultraMobileTokenSDK/Push/WMTPush.swift index 28bc229..37a6f46 100644 --- a/WultraMobileTokenSDK/Push/WMTPush.swift +++ b/WultraMobileTokenSDK/Push/WMTPush.swift @@ -29,11 +29,54 @@ public protocol WMTPush: AnyObject { /// Registers the current powerauth activation for push notifications. /// + /// This method is compatible with server stack `1.9.x` + /// /// - Parameters: /// - token: Push token. /// - completion: Completion handler. /// This completion is always called on the main thread. /// - Returns: Operation object for its state observation. @discardableResult + @available(*, deprecated, renamed: "register", message: "This method is deprecated since server version 1.10.0. Use register(token:completion:) instead.") func registerDeviceTokenForPushNotifications(token: Data, completion: @escaping (Result) -> Void) -> Operation? + + /// Registers the current powerauth activation for push notifications. + /// + /// - Parameters: + /// - platform: Platform that you're registering to + /// - completion: Completion handler. + /// This completion is always called on the main thread. + /// - Returns: Operation object for its state observation. + @discardableResult + func register(to platform: WMTPushPlatform, completion: @escaping (Result) -> Void) -> Operation? +} + +/// Push platform that is used for push notifications +public enum WMTPushPlatform { + + /// Apple Push Notification Service - when you're using directly Apple Push Service for push notifications + /// - Parameters: + /// - token: APNS push token data retrieved from the system + /// - environment: APNS push environment. Default value is `automatic` + case apns(token: Data, environment: WMTPushAPNSEnvironment = .automatic) + + /// Firebase Cloud Messaging - when you're using Firebase to send push notifications + /// - Parameters: + /// - token: FCM token retrieved from the Firebase SDK + case fcm(token: String) +} + +/// APNS push environment. +/// +/// Production environment (server) must be used for production signed apps. (For example TestFlight or AppStore distrubution). +/// Development environment (server) must be used for developer-signed apps. (For example debug builds or ad-hoc development distrubition). +public enum WMTPushAPNSEnvironment { + /// Automatically detect how is the app signed and set the environment properly. + /// + /// When automatic detection fails, environment is not sent at all and server configuration is used. + case automatic + /// Production APNS environment for production-signed app (TestFlight and AppStore distribution). + case production + /// Development signed app. + case development } diff --git a/WultraMobileTokenSDKTests/Configs/Readme.md b/WultraMobileTokenSDKTests/Configs/Readme.md index 701248a..fe83a36 100644 --- a/WultraMobileTokenSDKTests/Configs/Readme.md +++ b/WultraMobileTokenSDKTests/Configs/Readme.md @@ -11,6 +11,7 @@ _Example config:_ "enrollmentServerUrl" : "https://url-to-my-cloud.com/enrollment-server", "operationsServerUrl" : "https://url-to-my-cloud.com/enrollment-server", "inboxServerUrl" : "https://url-to-my-cloud.com/enrollment-server", + "pushServerUrl" : "https://url-to-my-cloud.com/enrollment-server", "sdkConfig" : "PB5YVmaON739UFGyBfog274wr3EJdcMLzVikqJMKOoPz+O11+45YGIPq+2z8L0p43LC/IVKrViJ+v1SHc1/PwPrtCsCQ5FX4fOFOJEFZZPLFs=" } ``` diff --git a/WultraMobileTokenSDKTests/IntegrationProxy.swift b/WultraMobileTokenSDKTests/IntegrationProxy.swift index b0497df..3fef384 100644 --- a/WultraMobileTokenSDKTests/IntegrationProxy.swift +++ b/WultraMobileTokenSDKTests/IntegrationProxy.swift @@ -23,6 +23,7 @@ class IntegrationProxy { private(set) var powerAuth: PowerAuthSDK? private(set) var operations: WMTOperations? private(set) var inbox: WMTInbox? + private(set) var push: WMTPush? private var config: IntegrationConfig! private let activationName = UUID().uuidString @@ -30,10 +31,11 @@ class IntegrationProxy { typealias Callback = (_ error: String?) -> Void - func prepareActivation(pin: String, callback: @escaping Callback) { + func prepareActivation(pin: String, configFileName: String = "config", callback: @escaping Callback) { WPNLogger.verboseLevel = .debug - guard let configPath = Bundle.init(for: IntegrationProxy.self).path(forResource: "config", ofType: "json", inDirectory: "Configs") else { - callback("Config file config.json is not present.") + + guard let configPath = Bundle.init(for: IntegrationProxy.self).path(forResource: configFileName, ofType: "json", inDirectory: "Configs") else { + callback("Config file \(configFileName).json is not present.") return } @@ -41,7 +43,7 @@ class IntegrationProxy { let configContent = try String(contentsOfFile: configPath) config = try JSONDecoder().decode(IntegrationConfig.self, from: configContent.data(using: .utf8)!) } catch _ { - callback("Config file config.json cannot be parsed.") + callback("Config file \(configFileName).json cannot be parsed.") return } @@ -52,9 +54,11 @@ class IntegrationProxy { } else { let wpnOperationsConf = WPNConfig(baseUrl: URL(string: self.config.operationsServerUrl)!, sslValidation: .noValidation) let wpnInboxConf = WPNConfig(baseUrl: URL(string: self.config.inboxServerUrl)!, sslValidation: .noValidation) + let wpnPushConf = WPNConfig(baseUrl: URL(string: self.config.pushServerUrl)!, sslValidation: .noValidation) self.powerAuth = pa self.operations = pa.createWMTOperations(networkingConfig: wpnOperationsConf, pollingOptions: [.pauseWhenOnBackground]) self.inbox = pa.createWMTInbox(networkingConfig: wpnInboxConf) + self.push = pa.createWMTPush(networkingConfig: wpnPushConf) callback(nil) } } @@ -302,6 +306,7 @@ private struct IntegrationConfig: Codable { let enrollmentServerUrl: String let operationsServerUrl: String let inboxServerUrl: String + let pushServerUrl: String let sdkConfig: String } diff --git a/WultraMobileTokenSDKTests/IntegrationTests.swift b/WultraMobileTokenSDKTests/IntegrationTests.swift index a38e71c..07341be 100644 --- a/WultraMobileTokenSDKTests/IntegrationTests.swift +++ b/WultraMobileTokenSDKTests/IntegrationTests.swift @@ -18,7 +18,6 @@ import XCTest import PowerAuth2 @testable import WultraMobileTokenSDK - /** For integration test to be successfully executed, you need to provide configuration json file. To more information, visit `WultraMobileTokenSDKTests/Configs/Readme.md`. @@ -30,6 +29,7 @@ class IntegrationTests: XCTestCase { private var pa: PowerAuthSDK! { proxy.powerAuth } private var ops: WMTOperations! { proxy.operations } private var inbox: WMTInbox! { proxy.inbox } + private var push: WMTPush! { proxy.push} private let pin = "1234" @@ -42,7 +42,7 @@ class IntegrationTests: XCTestCase { // Integration Utils prepares an valid activation and sets is as primary // token activation on nextstep server - proxy.prepareActivation(pin: pin) { error in + proxy.prepareActivation(pin: pin/*, configFileName: "config-stable"*/) { error in if let error = error { XCTFail(error) } @@ -138,37 +138,6 @@ class IntegrationTests: XCTestCase { waitForExpectations(timeout: 20, handler: nil) } - /// Test of the Operation cancel - func testDetailCancel() { - let exp = expectation(description: "Cancel operation detail") - - proxy.createNonPersonalisedPACOperation { op in - if let op { - DispatchQueue.main.async { - guard let operation = self.ops.getDetail(operationId: op.operationId, completion: { _ in - XCTFail("Operation should be already canceled") - exp.fulfill() - }) else { - XCTFail("Failed to create operation") - exp.fulfill() - return - } - - operation.cancel() - - // Allowing most of the timeout duration for potential completion of the getDetail call. - DispatchQueue.main.asyncAfter(deadline: .now() + 4) { - XCTAssertTrue(operation.isCancelled, "Operation should be cancelled") - exp.fulfill() - } - } - } - } - - // Wait for expectation to be fulfilled - waitForExpectations(timeout: 5, handler: nil) - } - func testOperationCanceledWithReason() { let exp = expectation(description: "Cancel operation with reason") let cancelReason = "PREARRANGED_REASON" @@ -765,8 +734,65 @@ class IntegrationTests: XCTestCase { } } } - // there are 3 backend calls, give it some time... - waitForExpectations(timeout: 20, handler: nil) + // there are severalstejn backend calls, give it some time... + waitForExpectations(timeout: 40, handler: nil) + } + + // MARK: - Push + + func testRegisterPushLegacy() { + let expect = expectation(description: "Register push legacy") + push.registerDeviceTokenForPushNotifications(token: "testtoken".data(using: .utf8)!) { result in + if case .failure(let error) = result { + XCTFail("Failed to register push legacy: \(error.description)") + } + expect.fulfill() + } + XCTWaiter().wait(for: [expect], timeout: 20) + } + + func testRegisterPushApns() { + let expect = expectation(description: "Register push APNS") + push.register(to: .apns(token: "testtoken".data(using: .utf8)!)) { result in + if case .failure(let error) = result { + XCTFail("Failed to register APNS push: \(error)") + } + expect.fulfill() + } + XCTWaiter().wait(for: [expect], timeout: 20) + } + + func testRegisterPushApnsProduction() { + let expect = expectation(description: "Register push APNS") + push.register(to: .apns(token: "testtoken".data(using: .utf8)!, environment: .production)) { result in + if case .failure(let error) = result { + XCTFail("Failed to register APNS push: \(error)") + } + expect.fulfill() + } + XCTWaiter().wait(for: [expect], timeout: 20) + } + + func testRegisterPushApnsDevelopment() { + let expect = expectation(description: "Register push APNS") + push.register(to: .apns(token: "testtoken".data(using: .utf8)!, environment: .development)) { result in + if case .failure(let error) = result { + XCTFail("Failed to register APNS push: \(error)") + } + expect.fulfill() + } + XCTWaiter().wait(for: [expect], timeout: 20) + } + + func testRegisterPushFcm() { + let expect = expectation(description: "Register push APNS") + push.register(to: .fcm(token: "testtoken")) { result in + if case .failure(let error) = result { + XCTFail("Failed to register FCM push: \(error)") + } + expect.fulfill() + } + XCTWaiter().wait(for: [expect], timeout: 20) } // MARK: - Inbox diff --git a/WultraMobileTokenSDKTests/NetworkingObjectsTests.swift b/WultraMobileTokenSDKTests/NetworkingObjectsTests.swift index 4c2d54d..e72a0ea 100644 --- a/WultraMobileTokenSDKTests/NetworkingObjectsTests.swift +++ b/WultraMobileTokenSDKTests/NetworkingObjectsTests.swift @@ -34,15 +34,66 @@ class NetworkingObjectsTests: XCTestCase { super.tearDown() } - func testTokenRequest() { + func testTokenRequestLegacy() { let expectation = """ {"requestObject":{"platform":"ios","token":"5FBC85D026945C48A17FE1327C68C77F7793FEBFE23FF5850224BEE4215C5525"}} """ - let r = WMTPushEndpoints.RegisterDevice.EndpointType.RequestData(WMTPushRegistrationData(token: "5FBC85D026945C48A17FE1327C68C77F7793FEBFE23FF5850224BEE4215C5525")) + let r = WMTPushEndpoints.RegisterDevice.EndpointType.RequestData(WMTPushRegistrationData(platform: .ios, token: "5FBC85D026945C48A17FE1327C68C77F7793FEBFE23FF5850224BEE4215C5525", environment: nil)) r.testSerialization(expectation: expectation) } + func testTokenRequestLegacyWithEnvironment() { + let expectation = """ + {"requestObject":{"platform":"ios","token":"5FBC85D026945C48A17FE1327C68C77F7793FEBFE23FF5850224BEE4215C5525","environment":"development"}} + """ + let r = WMTPushEndpoints.RegisterDevice.EndpointType.RequestData(WMTPushRegistrationData(platform: .ios, token: "5FBC85D026945C48A17FE1327C68C77F7793FEBFE23FF5850224BEE4215C5525", environment: .development)) + + r.testSerialization(expectation: expectation) + } + + func testTokenRequestApnsDevelopment() { + let expectation = """ + {"requestObject":{"platform":"apns","token":"5FBC85D026945C48A17FE1327C68C77F7793FEBFE23FF5850224BEE4215C5525","environment":"development"}} + """ + let r = WMTPushEndpoints.RegisterDevice.EndpointType.RequestData(WMTPushRegistrationData(platform: .apns, token: "5FBC85D026945C48A17FE1327C68C77F7793FEBFE23FF5850224BEE4215C5525", environment: .development)) + + r.testSerialization(expectation: expectation) + } + + func testTokenRequestApnsProduction() { + let expectation = """ + {"requestObject":{"platform":"apns","token":"5FBC85D026945C48A17FE1327C68C77F7793FEBFE23FF5850224BEE4215C5525","environment":"production"}} + """ + let r = WMTPushEndpoints.RegisterDevice.EndpointType.RequestData(WMTPushRegistrationData(platform: .apns, token: "5FBC85D026945C48A17FE1327C68C77F7793FEBFE23FF5850224BEE4215C5525", environment: .production)) + + r.testSerialization(expectation: expectation) + } + + func testTokenRequestFcm() { + let expectation = """ + {"requestObject":{"platform":"fcm","token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1"}} + """ + let r = WMTPushEndpoints.RegisterDevice.EndpointType.RequestData(WMTPushRegistrationData(platform: .fcm, token: "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1", environment: nil)) + + r.testSerialization(expectation: expectation) + } + + func testApnsTokenSerialization() { + let apnsData = "testData".data(using: .utf8)! + let expectedApnsHex = "7465737444617461" + let apns = WMTPushPlatform.apns(token: apnsData, environment: .automatic) + // data should be transformed to hexformat + XCTAssertEqual(expectedApnsHex, apns.token) + } + + func testFcmTokenSerialization() { + let fcmData = "testToken" + let fcm = WMTPushPlatform.fcm(token: fcmData) + // FCM token should be the same + XCTAssertEqual(fcmData, fcm.token) + } + func testOperationsResponse() { let response = """ {"status":"OK","currentTimestamp":"2023-02-10T12:30:42+0000","responseObject":[{"id":"930febe7-f350-419a-8bc0-c8883e7f71e3","name":"authorize_payment","data":"A1*A100CZK*Q238400856/0300**D20170629*NUtility Bill Payment - 05/2017","status":"PENDING","operationCreated":"2018-08-08T12:30:42+0000","operationExpires":"2018-08-08T12:35:43+0000","allowedSignatureType":{"type":"2FA","variants":["possession_knowledge", "possession_biometry"]},"formData":{"title":"Potvrzení platby","message":"Dobrý den,prosíme o potvrzení následující platby:","attributes":[{"type":"AMOUNT","id":"operation.amount","label":"Částka","amount":965165234082.23,"currency":"CZK", "valueFormatted": "965165234082.23 CZK"},{"type":"KEY_VALUE","id":"operation.account","label":"Na účet","value":"238400856/0300"},{"type":"KEY_VALUE","id":"operation.dueDate","label":"Datum splatnosti","value":"29.6.2017"},{"type":"NOTE","id":"operation.note","label":"Poznámka","note":"Utility Bill Payment - 05/2017"},{"type":"PARTY_INFO","id":"operation.partyInfo","label":"Application","partyInfo":{"logoUrl":"http://whywander.com/wp-content/uploads/2017/05/prague_hero-100x100.jpg","name":"Tesco","description":"Objevte více příběhů psaných s chutí","websiteUrl":"https://itesco.cz/hello/vse-o-jidle/pribehy-psane-s-chuti/clanek/tomovy-burgery-pro-zapalene-fanousky/15012"}},{ "type": "AMOUNT_CONVERSION", "id": "operation.conversion", "label": "Conversion", "dynamic": true, "sourceAmount": 1.26, "sourceCurrency": "ETC", "sourceAmountFormatted": "1.26", "sourceCurrencyFormatted": "ETC", "sourceValueFormatted": "1.26 ETC", "targetAmount": 1710.98, "targetCurrency": "USD", "targetAmountFormatted": "1,710.98", "targetCurrencyFormatted": "USD", "targetValueFormatted": "1,710.98 USD"},{ "type": "IMAGE", "id": "operation.image", "label": "Image", "thumbnailUrl": "https://example.com/123_thumb.jpeg", "originalUrl": "https://example.com/123.jpeg" },{ "type": "IMAGE", "id": "operation.image", "label": "Image", "thumbnailUrl": "https://example.com/123_thumb.jpeg" }]}},{"id":"930febe7-f350-419a-8bc0-c8883e7f71e3","name":"authorize_payment","data":"A1*A100CZK*Q238400856/0300**D20170629*NUtility Bill Payment - 05/2017","status":"PENDING","operationCreated":"2018-08-08T12:30:42+0000","operationExpires":"2018-08-08T12:35:43+0000","allowedSignatureType":{"type":"1FA","variants":["possession_knowledge"]},"formData":{"title":"Potvrzení platby","message":"Dobrý den,prosíme o potvrzení následující platby:","attributes":[{"type":"AMOUNT","id":"operation.amount","label":"Částka","amount":100,"currency":"CZK"},{"type":"KEY_VALUE","id":"operation.account","label":"Na účet","value":"238400856/0300"},{"type":"KEY_VALUE","id":"operation.dueDate","label":"Datum splatnosti","value":"29.6.2017"},{"type":"NOTE","id":"operation.note","label":"Poznámka","note":"Utility Bill Payment - 05/2017"}]}}]} @@ -206,41 +257,41 @@ class NetworkingObjectsTests: XCTestCase { XCTAssertEqual("USD", conversionAttr.target.currencyFormatted) } - func testAmountAndConversionAttributesOnlyFormattedValues() { - let json = """ - {"status":"OK", "currentTimestamp":"2023-02-10T12:30:42+0000", "responseObject":[{"id":"930febe7-f350-419a-8bc0-c8883e7f71e3", "name":"authorize_payment", "status":"PENDING", "data":"A1*A100CZK*Q238400856/0300**D20170629*NUtility Bill Payment - 05/2017", "operationCreated":"2018-08-08T12:30:42+0000", "operationExpires":"2018-08-08T12:35:43+0000", "allowedSignatureType": {"type":"2FA", "variants": ["possession_knowledge", "possession_biometry"]}, "formData": {"title":"Potvrzení platby", "message":"Dobrý den,prosíme o potvrzení následující platby:", "attributes": [{"type":"AMOUNT", "id":"operation.amount", "label":"Částka", "amountFormatted":"965165234082.23", "currencyFormatted":"CZK"}, { "type": "AMOUNT_CONVERSION", "id": "operation.conversion", "label": "Conversion", "dynamic": true, "sourceAmountFormatted": "1.26", "sourceCurrencyFormatted": "ETC", "targetAmountFormatted": "1710.98", "targetCurrencyFormatted": "USD"}]}}]} - """.trimmingCharacters(in: .whitespacesAndNewlines) + func testAmountAndConversionAttributesOnlyFormattedValues() { + let json = """ + {"status":"OK", "currentTimestamp":"2023-02-10T12:30:42+0000", "responseObject":[{"id":"930febe7-f350-419a-8bc0-c8883e7f71e3", "name":"authorize_payment", "status":"PENDING", "data":"A1*A100CZK*Q238400856/0300**D20170629*NUtility Bill Payment - 05/2017", "operationCreated":"2018-08-08T12:30:42+0000", "operationExpires":"2018-08-08T12:35:43+0000", "allowedSignatureType": {"type":"2FA", "variants": ["possession_knowledge", "possession_biometry"]}, "formData": {"title":"Potvrzení platby", "message":"Dobrý den,prosíme o potvrzení následující platby:", "attributes": [{"type":"AMOUNT", "id":"operation.amount", "label":"Částka", "amountFormatted":"965165234082.23", "currencyFormatted":"CZK"}, { "type": "AMOUNT_CONVERSION", "id": "operation.conversion", "label": "Conversion", "dynamic": true, "sourceAmountFormatted": "1.26", "sourceCurrencyFormatted": "ETC", "targetAmountFormatted": "1710.98", "targetCurrencyFormatted": "USD"}]}}]} + """.trimmingCharacters(in: .whitespacesAndNewlines) - guard let result = try? jsonDecoder.decode(WPNResponseArray.self, from: json.data(using: .utf8)!) else { - XCTFail("Failed to parse JSON data") - return - } + guard let result = try? jsonDecoder.decode(WPNResponseArray.self, from: json.data(using: .utf8)!) else { + XCTFail("Failed to parse JSON data") + return + } - guard let amountAttr = result.responseObject?[0].formData.attributes[0] as? WMTOperationAttributeAmount else { - XCTFail("amount attribute not recognized") - return - } + guard let amountAttr = result.responseObject?[0].formData.attributes[0] as? WMTOperationAttributeAmount else { + XCTFail("amount attribute not recognized") + return + } - XCTAssertNil(amountAttr.amount) - XCTAssertNil(amountAttr.currency) - XCTAssertEqual("965165234082.23", amountAttr.amountFormatted) - XCTAssertEqual("CZK", amountAttr.currencyFormatted) + XCTAssertNil(amountAttr.amount) + XCTAssertNil(amountAttr.currency) + XCTAssertEqual("965165234082.23", amountAttr.amountFormatted) + XCTAssertEqual("CZK", amountAttr.currencyFormatted) - guard let conversionAttr = result.responseObject?[0].formData.attributes[1] as? WMTOperationAttributeAmountConversion else { - XCTFail("conversion attribute not recognized") - return - } + guard let conversionAttr = result.responseObject?[0].formData.attributes[1] as? WMTOperationAttributeAmountConversion else { + XCTFail("conversion attribute not recognized") + return + } - XCTAssertNil(conversionAttr.source.amount) - XCTAssertNil(conversionAttr.source.currency) - XCTAssertNil(conversionAttr.target.amount) - XCTAssertNil(conversionAttr.target.currency) + XCTAssertNil(conversionAttr.source.amount) + XCTAssertNil(conversionAttr.source.currency) + XCTAssertNil(conversionAttr.target.amount) + XCTAssertNil(conversionAttr.target.currency) - XCTAssertEqual("1.26", conversionAttr.source.amountFormatted) - XCTAssertEqual("ETC", conversionAttr.source.currencyFormatted) - XCTAssertEqual("1710.98", conversionAttr.target.amountFormatted) - XCTAssertEqual("USD", conversionAttr.target.currencyFormatted) - } + XCTAssertEqual("1.26", conversionAttr.source.amountFormatted) + XCTAssertEqual("ETC", conversionAttr.source.currencyFormatted) + XCTAssertEqual("1710.98", conversionAttr.target.amountFormatted) + XCTAssertEqual("USD", conversionAttr.target.currencyFormatted) + } func testErrorResponse() { diff --git a/WultraMobileTokenSDKTests/ProvisioningUtilsTests.swift b/WultraMobileTokenSDKTests/ProvisioningUtilsTests.swift new file mode 100644 index 0000000..1e7367d --- /dev/null +++ b/WultraMobileTokenSDKTests/ProvisioningUtilsTests.swift @@ -0,0 +1,70 @@ +// +// Copyright 2020 Wultra s.r.o. +// +// 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 XCTest +@testable import WultraMobileTokenSDK + +class ProvisioningUtilsTests: XCTestCase { + + // provisioning profile parsing + + func testParseProvisioningProfile() { + guard let data = Data(base64Encoded: base64ProductionProfile) else { + XCTFail("Could not decode base64 profile") + return + } + let profile = WMTProvisioningUtils.getProvisioningProfileFromData(data) + XCTAssertEqual(profile?.entitlements.apsEnvironment, .production) + } + + // plist parsing + + func testParseDevelopmentAPNS() { + let plist = getPlist("development") + let profile = WMTProvisioningUtils.parseProvisioningProfilePlist(plist) + XCTAssertEqual(profile?.entitlements.apsEnvironment, .development) + } + + func testParseProductionAPNS() { + let plist = getPlist("production") + let profile = WMTProvisioningUtils.parseProvisioningProfilePlist(plist) + XCTAssertEqual(profile?.entitlements.apsEnvironment, .production) + } + + func testParseMissingAPNS() { + let plist = getPlist(nil) + let profile = WMTProvisioningUtils.parseProvisioningProfilePlist(plist) + XCTAssertEqual(profile?.entitlements.apsEnvironment, nil) + } + + func testParseUnknownAPNS() { + let plist = getPlist("integration") // unknown value + let profile = WMTProvisioningUtils.parseProvisioningProfilePlist(plist) + XCTAssertEqual(profile?.entitlements.apsEnvironment, nil) + } + + private func getPlist(_ apnsEnvironment: String?) -> Data { + let entry = if let apnsEnvironment { + "aps-environment\(apnsEnvironment)" + } else { + "" + } + return " AppIDName for testing purposes ApplicationIdentifierPrefix ASDASDASD CreationDate 2024-01-08T10:59:17Z Platform iOS xrOS visionOS IsXcodeManaged DeveloperCertificates pqi3u4hrjkanfkjanldfjkansd;fiohLmFwcC5Nb2JpbGVUb2tlbi5kZXYwggEIDBVEZXZlbG9wZXJDZXJ0aWZpY2F0ZXMwge4EILJ8BwOI38tdzUcaq6hIvRRO6D2QmN9HDEf7QPB+6axsBCBvpLxOYiaJykpTpjoFwyvfZclkQGG4S2Cw0HC8qrenSwQgkdZR2h310zeFWcJiJua9POg+E1qI0DXyAH5rGHXbnQsEIMr0/eY+Qqyujx1sNvG12cizX5KaudM3CjtzvsVqIKnkBCDccTpELoQR0s/FEqUpCem1yCEqtgKE8kD3kOAdggSYJQQgxcz/0oI5Y8l0uI7f5zgaT1wi3WomccHWmtoOq3HPyjIEIElhcDeOADtptU1rQvX21BVbyIKYOwwEGDa1bJ+QeATEMIIBiwwMRW50aXRsZW1lbnRzcIIBeQIBAbCCAXIwQwwWYXBwbGljYXRpb24taWRlbnRpZmllcgwpS1RUOUc4NTlNUi5jb20ud3VsdHJhLmFwcC5Nb2JpbGVUb2tlbi5kZXYwHgwPYXBzLWVudmlyb25tZW50DAtkZXZlbG9wbWVudDAxDCNjb20uYXBwbGUuZGV2ZWxvcGVyLnRlYW0taWRlbnRpZmllcgwKS1RUOUc4NTlNUjCBhwwlY29tLmFwcGxlLnNlY3VyaXR5LmFwcGxpY2F0aW9uLWdyb3VwczBeDBpncm91cC5jb20ud3VsdHJhLnRlc3RHcm91cAxAZ3JvdXAuQ1ktMEZDMzEwNUMtRUY2RC0xMUU5LTgyM0ItMzMxMzc0MDhCQzk2LmNvbS5jeWRpYS5FeHRlbmRlcjATDA5nZXQtdGFzay1hbGxvdwEB/zA5DBZrZXljaGFpbi1hY2Nlc3MtZ3JvdXBzMB8MDEtUVDlHODU5TVIuKgwPY29tLmFwcGxlLnRva2VuMIIGpQwSUHJvdmlzaW9uZWREZXZpY2VzMIIGjQwZMDAwMDgxMDEtMDAwNTI4RDkzQTEyMDAxRQwZMDAwMjelknfkjaerl;f DER-Encoded-Profile UftjJiXcnphFtPME8RWgD9WFgMpfUPLE0HRxN12peXl28xXO0rVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFLuw3qFYM4iapIqZ3r6966/ayySrMEYGCCsGAQUFBwEBBDowODA2BggrBgEFBQcwAYYqaHR0cDovL29jc3AuYXBwbGUuY29tL29jc3AwMy1hcHBsZXJvb3RjYWczMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly9jcmwuYXBwbGUuY29tL2FwcGxlcm9vdGNhZzMuY3JsMB0GA1UdDgQWBBR6R7o4ihUkSCJGzb6PGiR7NAMqaTAOBgNVHQ8BAf8EBAMCQcm9kdWN0cyBhbmQvb3IgQXBwbGUgcHJvY2Vzc2VzLjAdBgNVHQ4EFgQUDgWBWc9LzVC4LP5b4EGBqz8zz+8wDgYDVR0PAQH/BAQDAgeAMA8GCSqGSIb3Y2QMEwQCBQAwCgYIKoZIzj0EAwIDRwAwRAIgSl19xJZLrQqOZYa8t493EpwrI7/L2J8LhbwYpW7gO80CIGiD58K1x1h1Cp43EHeyVRIYiVBThZVwbxWxLF7W8pwuMYIB1zCCAdMCAQEwfjByMSYwJAYDVQQDDB1BcHBsZSBTeXN0ZW0gSW50ZWdyYXRpb24gQ0EgNDEmMCQGA1UECwwdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTAggQZ+4OarG1YjANBglghkgBZQMEAgEFAKCB6TAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yNDAxMDgxMDU5MTdaMCoGCSqGSIb3DQEJNDEdMBswDQYJYIZIAWUDBAIBBQChCgYIKoZIzj0EAwIwLwYJKoZIhvcNAQkEMSIEIMRi+YCgI1wI14ztb05ZvyGPMqw3KEKsLMcwLVtJ6yC7MFIGCSqGSIb3DQEJDzFFMEMwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3DQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMAoGCCqGSM49BAMCBEcwRQIhAMc5IyUcaFaKFEyjmo7hMiMGYYg6m+iJnLY6M4vc88yhAiB94O5nW+8T9zaGzCOB2Ey8VWEKTioOPyOSc8GZVntlHQ== Entitlements \(entry) com.apple.security.application-groups group.com.wultra.testGroup application-identifier ASDASDASD.com.wultra.test keychain-access-groups ASDASDASD.* com.apple.token get-task-allow com.apple.developer.team-identifier ASDASDASD ExpirationDate 2025-01-07T10:59:17Z Name iOS Team Provisioning Profile: com.wultra.test ProvisionedDevices 00008101-000528D93A12001E 00008101-001C34EC2250801E 00008030-000975CC0AEA802E 00008101-001269DE0A50001E TeamIdentifier ASDASDASD TeamName Wultra TimeToLive 365 UUID 7749747f-897e-45f3-b78e-d327a1c9f38a Version 1 ".data(using: .utf8)! + } + + // real base64 encoded embedded.mobileprovisioning from the testflight app with a profuction apns + private let base64ProductionProfile = "MIIxVAYJKoZIhvcNAQcCoIIxRTCCMUECAQExCzAJBgUrDgMCGgUAMIIhYQYJKoZIhvcNAQcBoIIhUgSCIU48P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCI/Pgo8IURPQ1RZUEUgcGxpc3QgUFVCTElDICItLy9BcHBsZS8vRFREIFBMSVNUIDEuMC8vRU4iICJodHRwOi8vd3d3LmFwcGxlLmNvbS9EVERzL1Byb3BlcnR5TGlzdC0xLjAuZHRkIj4KPHBsaXN0IHZlcnNpb249IjEuMCI+CjxkaWN0PgoJPGtleT5BcHBJRE5hbWU8L2tleT4KCTxzdHJpbmc+WEMgY29tIHd1bHRyYSBhcHAgTW9iaWxlVG9rZW48L3N0cmluZz4KCTxrZXk+QXBwbGljYXRpb25JZGVudGlmaWVyUHJlZml4PC9rZXk+Cgk8YXJyYXk+Cgk8c3RyaW5nPktUVDlHODU5TVI8L3N0cmluZz4KCTwvYXJyYXk+Cgk8a2V5PkNyZWF0aW9uRGF0ZTwva2V5PgoJPGRhdGU+MjAyNC0xMC0yNFQxNzoyNTo0Nlo8L2RhdGU+Cgk8a2V5PlBsYXRmb3JtPC9rZXk+Cgk8YXJyYXk+CgkJPHN0cmluZz5pT1M8L3N0cmluZz4KCQk8c3RyaW5nPnhyT1M8L3N0cmluZz4KCQk8c3RyaW5nPnZpc2lvbk9TPC9zdHJpbmc+Cgk8L2FycmF5PgoJPGtleT5Jc1hjb2RlTWFuYWdlZDwva2V5PgoJPGZhbHNlLz4KCTxrZXk+RGV2ZWxvcGVyQ2VydGlmaWNhdGVzPC9rZXk+Cgk8YXJyYXk+CgkJPGRhdGE+TUlJRnlUQ0NCTEdnQXdJQkFnSVFCaDh3VkxVakRyQVIrcWJHcGJpWENUQU5CZ2txaGtpRzl3MEJBUXNGQURCMU1VUXdRZ1lEVlFRREREdEJjSEJzWlNCWGIzSnNaSGRwWkdVZ1JHVjJaV3h2Y0dWeUlGSmxiR0YwYVc5dWN5QkRaWEowYVdacFkyRjBhVzl1SUVGMWRHaHZjbWwwZVRFTE1Ba0dBMVVFQ3d3Q1J6TXhFekFSQmdOVkJBb01Da0Z3Y0d4bElFbHVZeTR4Q3pBSkJnTlZCQVlUQWxWVE1CNFhEVEkwTVRBeU5ERTNNVEF6TUZvWERUSTFNVEF5TkRFM01UQXlPVm93Z1k4eEdqQVlCZ29Ka2lhSmsvSXNaQUVCREFwTFZGUTVSemcxT1UxU01UY3dOUVlEVlFRRERDNUJjSEJzWlNCRWFYTjBjbWxpZFhScGIyNDZJRmQxYkhSeVlTQnpMbkl1Ynk0Z0tFdFVWRGxIT0RVNVRWSXBNUk13RVFZRFZRUUxEQXBMVkZRNVJ6ZzFPVTFTTVJZd0ZBWURWUVFLREExWGRXeDBjbUVnY3k1eUxtOHVNUXN3Q1FZRFZRUUdFd0pEV2pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTzBxY21uRGt6R0R4dExOUmREcWJmNkVnVjF5SWFXZDlxRWtETWhuSkFPMDlzUGh5eVM2dHNnaVB6ZExocUJGZmxBcXNad2Fnd0p5M1piMkFHbEpVZmM0b1VrV0MrLzQyQmlKM3QybzJ2L0ttcnBVZlkxcU9sZjdlTFZ5cVdYRVVJWGZtKzI4dkMyR2ZJeWZKSzdsWU1kMlpxY1pScnZqb1h1TzVKS3NmckNUTzZvQWZHSkJINy9pbDFXcitHZi9vbXkzOTRKdzIzb1g0dTNqOExPR254akRpemlDTHArMTlNNWhIeEVPWjJBZzFqbDR2SVFJZGRIbzAxaUhpWkJUNjNENjFJcmNNejViM0FDRjVrR2RzbS9HU2VMdzkvSXI5YlF0cUQrUkFHTFo3KzN5ODN0OERSRE1rQXhwWUc1VE9jRlpJZWhObVB5VkQxQlF0L2lUTEtzQ0F3RUFBYU9DQWpnd2dnSTBNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVVDZjdBRlpENXIyUUtraEs1SmloakRKZnNwN0l3Y0FZSUt3WUJCUVVIQVFFRVpEQmlNQzBHQ0NzR0FRVUZCekFDaGlGb2RIUndPaTh2WTJWeWRITXVZWEJ3YkdVdVkyOXRMM2QzWkhKbk15NWtaWEl3TVFZSUt3WUJCUVVITUFHR0pXaDBkSEE2THk5dlkzTndMbUZ3Y0d4bExtTnZiUzl2WTNOd01ETXRkM2RrY21jek1EVXdnZ0VlQmdOVkhTQUVnZ0VWTUlJQkVUQ0NBUTBHQ1NxR1NJYjNZMlFGQVRDQi96Q0J3d1lJS3dZQkJRVUhBZ0l3Z2JZTWdiTlNaV3hwWVc1alpTQnZiaUIwYUdseklHTmxjblJwWm1sallYUmxJR0o1SUdGdWVTQndZWEowZVNCaGMzTjFiV1Z6SUdGalkyVndkR0Z1WTJVZ2IyWWdkR2hsSUhSb1pXNGdZWEJ3YkdsallXSnNaU0J6ZEdGdVpHRnlaQ0IwWlhKdGN5QmhibVFnWTI5dVpHbDBhVzl1Y3lCdlppQjFjMlVzSUdObGNuUnBabWxqWVhSbElIQnZiR2xqZVNCaGJtUWdZMlZ5ZEdsbWFXTmhkR2x2YmlCd2NtRmpkR2xqWlNCemRHRjBaVzFsYm5SekxqQTNCZ2dyQmdFRkJRY0NBUllyYUhSMGNITTZMeTkzZDNjdVlYQndiR1V1WTI5dEwyTmxjblJwWm1sallYUmxZWFYwYUc5eWFYUjVMekFXQmdOVkhTVUJBZjhFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVV5OC9ZOVZLbVpGNjBtTm9hK2NvQ3dDM3JtQjh3RGdZRFZSMFBBUUgvQkFRREFnZUFNQk1HQ2lxR1NJYjNZMlFHQVFjQkFmOEVBZ1VBTUJNR0NpcUdTSWIzWTJRR0FRUUJBZjhFQWdVQU1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQVVaZmthZlFnOFFrMXpYd3NaUk9sWEVidG5rTXkxWUlHUlRxd3NSd3dlVUc0akNKeXRXQ2lWQTRoOG8wWHIzU3lsQjJBOHFMNFZxVHdSckg4NXlwcEJmN2xHRFEwc0E1V0YwTnZxbndVSTlvd1ZQdjg1WkVORmo1OVgyUEJjVDE0S0VIRjNOY1ZHdWl3SmIvL24zNnk0bFVxNFRoS0pBZmQvYVpyUlVuRjlNSTAvWm0vSVVqbTBaNUpxR1RHdUh6bXVtYVk5bFQ2enVlYkZycVVNbE1QQWd2eW9MZjFHaGlOZXA4RU1VK045TGxDcE1KZ3FTUm43eSs2M1VOOFk4OTdqRzVHcDdmUUw3V3BUdjAyWVg5dFp5WFFwSi9rUEpyNmZTYkYyUU10NFl0ZldWQ3NkV3lrU0hRZUxYU21lNTMrOWt0RWZVZHpJUlRmK2hBc3ZCVzFxPC9kYXRhPgoJPC9hcnJheT4KCgk8a2V5PkRFUi1FbmNvZGVkLVByb2ZpbGU8L2tleT4KCTxkYXRhPk1JSU4wd1lKS29aSWh2Y05BUWNDb0lJTnhEQ0NEY0FDQVFFeER6QU5CZ2xnaGtnQlpRTUVBZ0VGQURDQ0E0MEdDU3FHU0liM0RRRUhBYUNDQTM0RWdnTjZNWUlEZGpBTURBZFdaWEp6YVc5dUFnRUJNQkFNQ2xScGJXVlViMHhwZG1VQ0FnRnNNQk1NRGtseldHTnZaR1ZOWVc1aFoyVmtBUUVBTUJjTUJFNWhiV1VNRDFkMWJIUnlZU0JMWlhrZ1VISnZaREFaREFoVVpXRnRUbUZ0WlF3TlYzVnNkSEpoSUhNdWNpNXZMakFkREF4RGNtVmhkR2x2YmtSaGRHVVhEVEkwTVRBeU5ERTNNalUwTmxvd0hnd09WR1ZoYlVsa1pXNTBhV1pwWlhJd0RBd0tTMVJVT1VjNE5UbE5VakFmREE1RmVIQnBjbUYwYVc5dVJHRjBaUmNOTWpVeE1ESTBNVGN4TURJNVdqQWdEQmRRY205bWFXeGxSR2x6ZEhKcFluVjBhVzl1Vkhsd1pRd0ZVMVJQVWtVd0lRd0lVR3hoZEdadmNtMHdGUXdEYVU5VERBUjRjazlUREFoMmFYTnBiMjVQVXpBcURBbEJjSEJKUkU1aGJXVU1IVmhESUdOdmJTQjNkV3gwY21FZ1lYQndJRTF2WW1sc1pWUnZhMlZ1TUNzTUcwRndjR3hwWTJGMGFXOXVTV1JsYm5ScFptbGxjbEJ5WldacGVEQU1EQXBMVkZRNVJ6ZzFPVTFTTUN3TUJGVlZTVVFNSkRCbU5UQTVOakUyTFRNeE16VXRORE01TUMwNFptUTBMVE13TmpNek1URXpaR1k0TWpBN0RCVkVaWFpsYkc5d1pYSkRaWEowYVdacFkyRjBaWE13SWdRZ0R4bzdseTBUdUQ1U3FKalpXbHNLRTFjVkRzR0kwdGR2VERMT2xkWWRsQ3N3Z2dHZ0RBeEZiblJwZEd4bGJXVnVkSE53Z2dHT0FnRUJzSUlCaHpBL0RCWmhjSEJzYVdOaGRHbHZiaTFwWkdWdWRHbG1hV1Z5RENWTFZGUTVSemcxT1UxU0xtTnZiUzUzZFd4MGNtRXVZWEJ3TGsxdlltbHNaVlJ2YTJWdU1CME1EMkZ3Y3kxbGJuWnBjbTl1YldWdWRBd0tjSEp2WkhWamRHbHZiakFZREJOaVpYUmhMWEpsY0c5eWRITXRZV04wYVhabEFRSC9NREVNSTJOdmJTNWhjSEJzWlM1a1pYWmxiRzl3WlhJdWRHVmhiUzFwWkdWdWRHbG1hV1Z5REFwTFZGUTVSemcxT1UxU01JR0hEQ1ZqYjIwdVlYQndiR1V1YzJWamRYSnBkSGt1WVhCd2JHbGpZWFJwYjI0dFozSnZkWEJ6TUY0TUdtZHliM1Z3TG1OdmJTNTNkV3gwY21FdWRHVnpkRWR5YjNWd0RFQm5jbTkxY0M1RFdTMHdSa016TVRBMVF5MUZSalpFTFRFeFJUa3RPREl6UWkwek16RXpOelF3T0VKRE9UWXVZMjl0TG1ONVpHbGhMa1Y0ZEdWdVpHVnlNQk1NRG1kbGRDMTBZWE5yTFdGc2JHOTNBUUVBTURrTUZtdGxlV05vWVdsdUxXRmpZMlZ6Y3kxbmNtOTFjSE13SHd3TVMxUlVPVWM0TlRsTlVpNHFEQTlqYjIwdVlYQndiR1V1ZEc5clpXNmdnZ2c4TUlJQ1F6Q0NBY21nQXdJQkFnSUlMY1g4aU5MRlM1VXdDZ1lJS29aSXpqMEVBd013WnpFYk1Ca0dBMVVFQXd3U1FYQndiR1VnVW05dmRDQkRRU0F0SUVjek1TWXdKQVlEVlFRTERCMUJjSEJzWlNCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVUTUJFR0ExVUVDZ3dLUVhCd2JHVWdTVzVqTGpFTE1Ba0dBMVVFQmhNQ1ZWTXdIaGNOTVRRd05ETXdNVGd4T1RBMldoY05Nemt3TkRNd01UZ3hPVEEyV2pCbk1Sc3dHUVlEVlFRRERCSkJjSEJzWlNCU2IyOTBJRU5CSUMwZ1J6TXhKakFrQmdOVkJBc01IVUZ3Y0d4bElFTmxjblJwWm1sallYUnBiMjRnUVhWMGFHOXlhWFI1TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekIyTUJBR0J5cUdTTTQ5QWdFR0JTdUJCQUFpQTJJQUJKanBMejFBY3FUdGt5SnlnUk1jM1JDVjhjV2pUbkhjRkJiWkR1V21CU3AzWkh0ZlRqalR1eHhFdFgvMUg3WXlZbDNKNllSYlR6QlBFVm9BL1ZoWURLWDFEeXhOQjBjVGRkcVhsNWR2TVZ6dEs1MTdJRHZZdVZUWlhwbWtPbEVLTWFOQ01FQXdIUVlEVlIwT0JCWUVGTHV3M3FGWU00aWFwSXFaM3I2OTY2L2F5eVNyTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3RGdZRFZSMFBBUUgvQkFRREFnRUdNQW9HQ0NxR1NNNDlCQU1EQTJnQU1HVUNNUUNENmNIRUZsNGFYVFFZMmUzdjlHd09BRVpMdU4reVJoSEZELzNtZW95aHBtdk93Z1BVblBXVHhuUzRhdCtxSXhVQ01HMW1paERLMUEzVVQ4Mk5RejYwaW1PbE0yN2piZG9YdDJRZnlGTW0rWWhpZERrTEYxdkxVYWdNNkJnRDU2S3lLRENDQXVZd2dnSnRvQU1DQVFJQ0NETU43dmkvVEdndU1Bb0dDQ3FHU000OUJBTURNR2N4R3pBWkJnTlZCQU1NRWtGd2NHeGxJRkp2YjNRZ1EwRWdMU0JITXpFbU1DUUdBMVVFQ3d3ZFFYQndiR1VnUTJWeWRHbG1hV05oZEdsdmJpQkJkWFJvYjNKcGRIa3hFekFSQmdOVkJBb01Da0Z3Y0d4bElFbHVZeTR4Q3pBSkJnTlZCQVlUQWxWVE1CNFhEVEUzTURJeU1qSXlNak15TWxvWERUTXlNREl4T0RBd01EQXdNRm93Y2pFbU1DUUdBMVVFQXd3ZFFYQndiR1VnVTNsemRHVnRJRWx1ZEdWbmNtRjBhVzl1SUVOQklEUXhKakFrQmdOVkJBc01IVUZ3Y0d4bElFTmxjblJwWm1sallYUnBiMjRnUVhWMGFHOXlhWFI1TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCQVpycEZadmZaOG4wYzQyanBJYlZzMVVObVJLeVpSb21mckpJSDdpOVZnUDNPSnE2eGxITHk3dk82UUJ0QUVUUkh4YUpxMmduQ2tsaXVYbUJtOVBmRnFqZ2Zjd2dmUXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWZCZ05WSFNNRUdEQVdnQlM3c042aFdET0ltcVNLbWQ2K3ZldXYyc3NrcXpCR0JnZ3JCZ0VGQlFjQkFRUTZNRGd3TmdZSUt3WUJCUVVITUFHR0ttaDBkSEE2THk5dlkzTndMbUZ3Y0d4bExtTnZiUzl2WTNOd01ETXRZWEJ3YkdWeWIyOTBZMkZuTXpBM0JnTlZIUjhFTURBdU1DeWdLcUFvaGlab2RIUndPaTh2WTNKc0xtRndjR3hsTG1OdmJTOWhjSEJzWlhKdmIzUmpZV2N6TG1OeWJEQWRCZ05WSFE0RUZnUVVla2U2T0lvVkpFZ2lSczIranhva2V6UURLbWt3RGdZRFZSMFBBUUgvQkFRREFnRUdNQkFHQ2lxR1NJYjNZMlFHQWhFRUFnVUFNQW9HQ0NxR1NNNDlCQU1EQTJjQU1HUUNNQlVNcVk3R3I1WnBhNmVmM1Z6VUExbHNybExVWU1hTGR1QzN4YUx4Q1h6Z211TnJzZU44TWNRbmVxZU9pZjJyZHdJd1lUTWc4U24vK1ljeXJpbklaRDEyZTFHazBnSXZkcjVnSXBIeDFUcDEzTFRpeGlxVy9zWUozRXBQMVNUdy9NcXlNSUlEQnpDQ0FxMmdBd0lCQWdJSVhLMG1KQk1ZQmI4d0NnWUlLb1pJemowRUF3SXdjakVtTUNRR0ExVUVBd3dkUVhCd2JHVWdVM2x6ZEdWdElFbHVkR1ZuY21GMGFXOXVJRU5CSURReEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QWVGdzB5TkRBeE1qa3hOalEzTURSYUZ3MHlPREF5TWpjeE5qUTNNRE5hTUU0eEtqQW9CZ05WQkFNTUlWZFhSRklnVUhKdmRtbHphVzl1YVc1bklGQnliMlpwYkdVZ1UybG5ibWx1WnpFVE1CRUdBMVVFQ2d3S1FYQndiR1VnU1c1akxqRUxNQWtHQTFVRUJoTUNWVk13V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVRFRGhQRHo2N3hFQzkrOVZUYlZHdUVCeC9qNWljUXFIVldpbFQ1QWE1L0k4VVNnSEFMNzJ1TUw3T3BEbjBCV2d0S05ZQWFHQWhibG5zcVpRZno0dDVFbzRJQlR6Q0NBVXN3REFZRFZSMFRBUUgvQkFJd0FEQWZCZ05WSFNNRUdEQVdnQlI2UjdvNGloVWtTQ0pHemI2UEdpUjdOQU1xYVRCQkJnZ3JCZ0VGQlFjQkFRUTFNRE13TVFZSUt3WUJCUVVITUFHR0pXaDBkSEE2THk5dlkzTndMbUZ3Y0d4bExtTnZiUzl2WTNOd01ETXRZWE5wWTJFME1ETXdnWllHQTFVZElBU0JqakNCaXpDQmlBWUpLb1pJaHZkalpBVUJNSHN3ZVFZSUt3WUJCUVVIQWdJd2JReHJWR2hwY3lCalpYSjBhV1pwWTJGMFpTQnBjeUIwYnlCaVpTQjFjMlZrSUdWNFkyeDFjMmwyWld4NUlHWnZjaUJtZFc1amRHbHZibk1nYVc1MFpYSnVZV3dnZEc4Z1FYQndiR1VnVUhKdlpIVmpkSE1nWVc1a0wyOXlJRUZ3Y0d4bElIQnliMk5sYzNObGN5NHdIUVlEVlIwT0JCWUVGR3YvWFFPVHV2SEY0cmowQ2piTTFFQ0w4WHdlTUE0R0ExVWREd0VCL3dRRUF3SUhnREFQQmdrcWhraUc5Mk5rREJNRUFnVUFNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJSGV6YjVqVmx3Uk1kV2N6TzlKQUoxSXJ0U041ZlRIN2ZIMVhXV3VlT2RMWkFpRUE3eFI4aExtQ1JTVnJIaUZoZCtkMnpEd3dJNWYyb2djRFFEck9FdkdkRFMweGdnSFhNSUlCMHdJQkFUQitNSEl4SmpBa0JnTlZCQU1NSFVGd2NHeGxJRk41YzNSbGJTQkpiblJsWjNKaGRHbHZiaUJEUVNBME1TWXdKQVlEVlFRTERCMUJjSEJzWlNCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVUTUJFR0ExVUVDZ3dLUVhCd2JHVWdTVzVqTGpFTE1Ba0dBMVVFQmhNQ1ZWTUNDRnl0SmlRVEdBVy9NQTBHQ1dDR1NBRmxBd1FDQVFVQW9JSHBNQmdHQ1NxR1NJYjNEUUVKQXpFTEJna3Foa2lHOXcwQkJ3RXdIQVlKS29aSWh2Y05BUWtGTVE4WERUSTBNVEF5TkRFM01qVTBObG93S2dZSktvWklodmNOQVFrME1SMHdHekFOQmdsZ2hrZ0JaUU1FQWdFRkFLRUtCZ2dxaGtqT1BRUURBakF2QmdrcWhraUc5dzBCQ1FReElnUWcxeC96UHA4OCt3dnpKY3dQYUc2diszR3ltcDlkMXdPUUpDSjBUL3VrWDFNd1VnWUpLb1pJaHZjTkFRa1BNVVV3UXpBS0JnZ3Foa2lHOXcwREJ6QU9CZ2dxaGtpRzl3MERBZ0lDQUlBd0RRWUlLb1pJaHZjTkF3SUNBVUF3QndZRkt3NERBZ2N3RFFZSUtvWklodmNOQXdJQ0FTZ3dDZ1lJS29aSXpqMEVBd0lFUnpCRkFpRUFwZlp5andXUENyUXB5NENQYU8rWXh0N0NPamJnNzZrMDNlVXArbEpVWTM4Q0lIbVBXd1dhVGxCbS8ramk5YjZDWXZvZTBabkdNY2EwdHNYZG9TYksyM1kvPC9kYXRhPgoJCQkJCQkJCQkJCQkKCTxrZXk+RW50aXRsZW1lbnRzPC9rZXk+Cgk8ZGljdD4KCQk8a2V5PmJldGEtcmVwb3J0cy1hY3RpdmU8L2tleT4KCQk8dHJ1ZS8+CgkJCQkKCQkJCTxrZXk+YXBzLWVudmlyb25tZW50PC9rZXk+CgkJPHN0cmluZz5wcm9kdWN0aW9uPC9zdHJpbmc+CgkJCQkKCQkJCTxrZXk+Y29tLmFwcGxlLnNlY3VyaXR5LmFwcGxpY2F0aW9uLWdyb3Vwczwva2V5PgoJCTxhcnJheT4KCQkJCTxzdHJpbmc+Z3JvdXAuY29tLnd1bHRyYS50ZXN0R3JvdXA8L3N0cmluZz4KCQkJCTxzdHJpbmc+Z3JvdXAuQ1ktMEZDMzEwNUMtRUY2RC0xMUU5LTgyM0ItMzMxMzc0MDhCQzk2LmNvbS5jeWRpYS5FeHRlbmRlcjwvc3RyaW5nPgoJCTwvYXJyYXk+CgkJCQkKCQkJCTxrZXk+YXBwbGljYXRpb24taWRlbnRpZmllcjwva2V5PgoJCTxzdHJpbmc+S1RUOUc4NTlNUi5jb20ud3VsdHJhLmFwcC5Nb2JpbGVUb2tlbjwvc3RyaW5nPgoJCQkJCgkJCQk8a2V5PmtleWNoYWluLWFjY2Vzcy1ncm91cHM8L2tleT4KCQk8YXJyYXk+CgkJCQk8c3RyaW5nPktUVDlHODU5TVIuKjwvc3RyaW5nPgoJCQkJPHN0cmluZz5jb20uYXBwbGUudG9rZW48L3N0cmluZz4KCQk8L2FycmF5PgoJCQkJCgkJCQk8a2V5PmdldC10YXNrLWFsbG93PC9rZXk+CgkJPGZhbHNlLz4KCQkJCQoJCQkJPGtleT5jb20uYXBwbGUuZGV2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" +} diff --git a/docs/Changelog.md b/docs/Changelog.md index 74a2b08..4d0fcf7 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,8 +1,9 @@ # Changelog -## 1.13.0 (TBA) +## X.X.X (TBA) - Added status to `UserOperation` and removed redundant `OperationHistoryEntry` [(#171)](https://github.com/wultra/mtoken-sdk-ios/pull/171) +- Added option for Firebase Cloud Messaging for Push Notifications and automatic APNS environment detection [(#174)](https://github.com/wultra/mtoken-sdk-ios/issues/174). ## 1.12.0 (October 2024) diff --git a/docs/Using-Push-Service.md b/docs/Using-Push-Service.md index 1ae7610..715c821 100644 --- a/docs/Using-Push-Service.md +++ b/docs/Using-Push-Service.md @@ -11,7 +11,7 @@ ## Introduction -Push Service is responsible for registering the device for the push notifications about the Operations that are tied to the current PowerAuth activation. +Push Service is responsible for registering the device for the push notifications about the operations that are tied to the current PowerAuth activation. Note: Before using Push Service, you need to have a `PowerAuthSDK` object available and initialized with a valid activation. Without a valid PowerAuth activation, the service will return an error @@ -48,28 +48,51 @@ All available methods of the `WMTPush` API are: - `pushNotificationsRegisteredOnServer` - If there was already made a successful request. - `acceptLanguage` - Language settings, that will be sent along with each request. -- `registerDeviceTokenForPushNotifications(token: Data, completionHandler: @escaping (_ success: Bool, _ error: WMTError?) -> Void)` - Registers push token on the backend. - - `token` - token data retrieved from APNS. +- `register(to: WMTPushPlatform, completionHandler: completion: @escaping (Result) -> Void)` - Registers push token on the backend. + - `to` - Platform and data needed (APNS or FCM + push token) - `completionHandler` - Called when the request finishes. Always called on the main thread. ## Registering to WMT Push Notifications -To register your app to push notifications regarding the operations, you can simply call the `registerDeviceTokenForPushNotifications` method: +### Using APNS (Apple Push Notification Service) + +To register your app to push notifications regarding the operations, you can simply call the `register` method with `.apns` platform parameter: ```swift // UIApplicationDelegate method func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - pushService.registerDeviceTokenForPushNotifications(token: deviceToken) { success, error in - guard success else { - // push registration failed - return + pushService.register(to: .apns(token: deviceToken)) { result in + if case .failure(let error) = result { + // registration failed + } + } +} +``` + + +The above method will get called only if you registered the app to receive push notifications. For more information, visit the [official apple documentation](https://developer.apple.com/documentation/usernotifications/handling_notifications_and_notification-related_actions). + + +### Using FCM (Firebase Cloud Messaging) + +To register your app to push notifications regarding the operations, you can simply call the `register` method with `.fcm` platform parameter: + +```swift +func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { + guard let fcmToken else { + // token not received + return + } + pushService.register(to: .fcm(token: fcmToken)) { result in + if case .failure(let error) = result { + // registration failed } } } ``` -The above method will get called only if you registered the app to receive push notifications. For more information, visit the [official documentation](https://developer.apple.com/documentation/usernotifications/handling_notifications_and_notification-related_actions). +To properly configure FCM for iOS, follow [official google documentation](https://firebase.google.com/docs/cloud-messaging/ios/client). ## Receiving WMT Push Notifications diff --git a/scripts/test.sh b/scripts/test.sh index ba911fe..d2ada43 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -14,6 +14,7 @@ CL_LGN="" CL_PWD="" CL_AID="" ER_URL="" +PU_URL="" OP_URL="" IN_URL="" SDKCONFIG="" @@ -57,6 +58,11 @@ do shift shift ;; + -pu) + PU_URL="$2" + shift + shift + ;; -in) IN_URL="$2" shift @@ -89,6 +95,7 @@ echo """{ \"cloudApplicationId\" : \"${CL_AID}\", \"enrollmentServerUrl\" : \"${ER_URL}\", \"operationsServerUrl\" : \"${OP_URL}\", + \"pushServerUrl\" : \"${PU_URL}\", \"inboxServerUrl\" : \"${IN_URL}\", \"sdkConfig\" : \"${SDKCONFIG}\" }""" > "WultraMobileTokenSDKTests/Configs/config.json"