Skip to content

Commit

Permalink
Merge pull request #32 from SwedbankPay/feature/state-saving
Browse files Browse the repository at this point in the history
Add support for view controller state saving.
  • Loading branch information
IhmeHippi authored Oct 21, 2021
2 parents ae2bef9 + cd71fe5 commit ac61321
Show file tree
Hide file tree
Showing 23 changed files with 1,751 additions and 259 deletions.
30 changes: 30 additions & 0 deletions SwedbankPaySDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@

/* Begin PBXBuildFile section */
6367D3EB26F340F700F89F62 /* TestConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6367D3EA26F340F700F89F62 /* TestConfiguration.swift */; };
63CB660D27171C2D00100683 /* ViewModelCodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CB660C27171C2D00100683 /* ViewModelCodingTests.swift */; };
63CB660F27171D4D00100683 /* ViewModelStateEquals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CB660E27171D4D00100683 /* ViewModelStateEquals.swift */; };
63CB66112719982800100683 /* ViewControllerRestorationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CB66102719982800100683 /* ViewControllerRestorationTests.swift */; };
63CB6613271D991200100683 /* Storyboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 63CB6612271D991200100683 /* Storyboard.storyboard */; };
63CB6615271D997100100683 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CB6614271D997100100683 /* ViewController.swift */; };
63EECFD0270730C800C37B69 /* CodableUserData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EECFCF270730C800C37B69 /* CodableUserData.swift */; };
63EECFF4270ED22D00C37B69 /* MockMerchantBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EECFF3270ED22D00C37B69 /* MockMerchantBackend.swift */; };
63EECFF6270F2DF500C37B69 /* ViewModelTestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EECFF5270F2DF500C37B69 /* ViewModelTestUtil.swift */; };
63EECFF82710742C00C37B69 /* ViewModelStateCodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EECFF72710742C00C37B69 /* ViewModelStateCodingTests.swift */; };
63EECFFA271080C300C37B69 /* ViewPaymentOrderInfoEquals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EECFF9271080C300C37B69 /* ViewPaymentOrderInfoEquals.swift */; };
63F5D69F26F0B23600C1F207 /* ConfigurationAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F5D69E26F0B23600C1F207 /* ConfigurationAsync.swift */; };
63F5D6A126F0C2A100C1F207 /* AsyncViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F5D6A026F0C2A100C1F207 /* AsyncViewModelTests.swift */; };
63F5D6A326F0C96F00C1F207 /* AsyncTestConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F5D6A226F0C96F00C1F207 /* AsyncTestConfiguration.swift */; };
Expand Down Expand Up @@ -153,9 +160,16 @@

/* Begin PBXFileReference section */
6367D3EA26F340F700F89F62 /* TestConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConfiguration.swift; sourceTree = "<group>"; };
63CB660C27171C2D00100683 /* ViewModelCodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelCodingTests.swift; sourceTree = "<group>"; };
63CB660E27171D4D00100683 /* ViewModelStateEquals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelStateEquals.swift; sourceTree = "<group>"; };
63CB66102719982800100683 /* ViewControllerRestorationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerRestorationTests.swift; sourceTree = "<group>"; };
63CB6612271D991200100683 /* Storyboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Storyboard.storyboard; sourceTree = "<group>"; };
63CB6614271D997100100683 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
63EECFCF270730C800C37B69 /* CodableUserData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableUserData.swift; sourceTree = "<group>"; };
63EECFF3270ED22D00C37B69 /* MockMerchantBackend.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockMerchantBackend.swift; sourceTree = "<group>"; };
63EECFF5270F2DF500C37B69 /* ViewModelTestUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelTestUtil.swift; sourceTree = "<group>"; };
63EECFF72710742C00C37B69 /* ViewModelStateCodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelStateCodingTests.swift; sourceTree = "<group>"; };
63EECFF9271080C300C37B69 /* ViewPaymentOrderInfoEquals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPaymentOrderInfoEquals.swift; sourceTree = "<group>"; };
63F5D69E26F0B23600C1F207 /* ConfigurationAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationAsync.swift; sourceTree = "<group>"; };
63F5D6A026F0C2A100C1F207 /* AsyncViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncViewModelTests.swift; sourceTree = "<group>"; };
63F5D6A226F0C96F00C1F207 /* AsyncTestConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncTestConfiguration.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -395,6 +409,9 @@
A57170DE2506531B00AC28BE /* FileUtils.swift */,
A542B35225249BA900AD8F09 /* Assertions.swift */,
6367D3EA26F340F700F89F62 /* TestConfiguration.swift */,
63EECFF5270F2DF500C37B69 /* ViewModelTestUtil.swift */,
63EECFF9271080C300C37B69 /* ViewPaymentOrderInfoEquals.swift */,
63CB660E27171D4D00100683 /* ViewModelStateEquals.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -474,6 +491,7 @@
A5DC2CA5251A2D730037C7DA /* ViewConsumerIdentificationInfo.swift */,
A5DC2CA9251A2DFA0037C7DA /* ViewPaymentOrderInfo.swift */,
63F5D69E26F0B23600C1F207 /* ConfigurationAsync.swift */,
63EECFCF270730C800C37B69 /* CodableUserData.swift */,
);
path = "SwedbankPaySDK+Extensions";
sourceTree = "<group>";
Expand Down Expand Up @@ -550,6 +568,9 @@
A596D670247FF7FD009605DB /* PaymentUrlTests.swift */,
A57170DC25064EF400AC28BE /* FileLinesTests.swift */,
A57170E025066D3A00AC28BE /* GoodWebViewRedirectsTests.swift */,
63EECFF72710742C00C37B69 /* ViewModelStateCodingTests.swift */,
63CB660C27171C2D00100683 /* ViewModelCodingTests.swift */,
63CB66102719982800100683 /* ViewControllerRestorationTests.swift */,
);
path = SwedbankPaySDKTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -852,6 +873,7 @@
A504895123C8A2FD00201DEC /* SwedbankPayWebContent.swift in Sources */,
C50CF69D237AACC3003F79DF /* Consumer.swift in Sources */,
A57170DA25011F8500AC28BE /* FileLines.swift in Sources */,
63EECFD0270730C800C37B69 /* CodableUserData.swift in Sources */,
A59AEE9D25372C9A00255A3A /* Instrument.swift in Sources */,
C585AF2F237066EE006C2E16 /* SwedbankPaySDKController.swift in Sources */,
63F5D69F26F0B23600C1F207 /* ConfigurationAsync.swift in Sources */,
Expand Down Expand Up @@ -879,22 +901,28 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
63EECFF82710742C00C37B69 /* ViewModelStateCodingTests.swift in Sources */,
A596D66D247EA39B009605DB /* SwedbankPaySDKControllerTestCase.swift in Sources */,
A596D671247FF7FD009605DB /* PaymentUrlTests.swift in Sources */,
A57170DD25064EF400AC28BE /* FileLinesTests.swift in Sources */,
A5535E7824755AE300BFD586 /* ViewModelTests.swift in Sources */,
63CB66112719982800100683 /* ViewControllerRestorationTests.swift in Sources */,
63EECFF4270ED22D00C37B69 /* MockMerchantBackend.swift in Sources */,
A57170E125066D3A00AC28BE /* GoodWebViewRedirectsTests.swift in Sources */,
A5535E7624754B2100BFD586 /* MockURLResult.swift in Sources */,
63CB660D27171C2D00100683 /* ViewModelCodingTests.swift in Sources */,
A542B35325249BA900AD8F09 /* Assertions.swift in Sources */,
A596D66F247FD164009605DB /* MissingStubError.swift in Sources */,
A596D66B247BF85B009605DB /* TestURLStubs.swift in Sources */,
A5535E7224754AD000BFD586 /* TestConstants.swift in Sources */,
63EECFFA271080C300C37B69 /* ViewPaymentOrderInfoEquals.swift in Sources */,
A596D669247BECD8009605DB /* ViewControllerTests.swift in Sources */,
A57170DF2506531B00AC28BE /* FileUtils.swift in Sources */,
A5535E832478078A00BFD586 /* MockURLProtocolExpectation.swift in Sources */,
63CB660F27171D4D00100683 /* ViewModelStateEquals.swift in Sources */,
A596D6732480FCA7009605DB /* ViewControllerConsumerTests.swift in Sources */,
A5535E812478072C00BFD586 /* StubbedURLError.swift in Sources */,
63EECFF6270F2DF500C37B69 /* ViewModelTestUtil.swift in Sources */,
63F5D6A326F0C96F00C1F207 /* AsyncTestConfiguration.swift in Sources */,
63F5D6A126F0C2A100C1F207 /* AsyncViewModelTests.swift in Sources */,
6367D3EB26F340F700F89F62 /* TestConfiguration.swift in Sources */,
Expand Down Expand Up @@ -1244,6 +1272,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = SwedbankPaySDKTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand All @@ -1266,6 +1295,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = SwedbankPaySDKTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
178 changes: 178 additions & 0 deletions SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/CodableUserData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//
// Copyright 2021 Swedbank AB
//
// 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

public extension SwedbankPaySDK {
/// To use a `Codable` type as the `userData` parameter for `SwedbankPaySDKController`,
/// or as the `userInfo` property of `SwedbankPaySDK.ViewPaymentOrderInfo`,
/// the type should be registered by calling this function. Failure to do so results
/// in exceptions being throw during state saving and/or restoration.
///
/// In addition, if you need lossless preservation of custom `Error` types as part of
/// `SwedbankPaySDKController` state preservation, you can register those types here as well.
/// Otherwise, `Error`s will be converted to `NSError` when saving and restoring the state.
///
/// The type should not be a private or local type. Use of such types may result in decoding failures.
static func registerCodable<T: Codable>(_ type: T.Type) {
registerCodable(type, encodedTypeName: defaultEncodedTypeName(for: type))
}

/// Variant of `registerCodable` that allows to manually set the encoded name for the `Codable` type.
///
/// If you must use a private or local type, then this function may help, as the default encoded name
/// for such types is unpredictable. Otherwise, there is ususaly no need to use this function.
///
/// Encoded type names beginning with `"com.swedbankpay."` are reserved for the SDK.
static func registerCodable<T: Codable>(_ type: T.Type, encodedTypeName: String) {
Coders.registerCoder(for: type, encodedTypeName: encodedTypeName)
}
}

private func defaultEncodedTypeName(for codableType: Codable.Type) -> String {
return String(reflecting: codableType)
}

private let internalEncodedTypeNamePrefix = "com.swedbankpay.mobilesdk."

extension KeyedEncodingContainer {
mutating func encodeIfPresent(userData: Any?, codableTypeKey: Key, valueKey: Key) throws {
switch userData {
case nil:
break
case let nsCodingUserData as NSCoding:
try encode(nsCodingUserData: nsCodingUserData, key: valueKey)
case let codableUserData as Codable:
try encode(codableUserData: codableUserData, typeKey: codableTypeKey, valueKey: valueKey)
default:
fatalError("userData must conform to Codable or NSCoding if you want to support state restoration")
}
}

mutating func encodeIfPresent(error: Error?, codableTypeKey: Key, valueKey: Key) throws {
if let codableError = error as? Codable, Coders.getCoder(for: codableError) != nil /*registeredEncoders[ObjectIdentifier(type(of: codableError))] != nil*/ {
try encode(codableUserData: codableError, typeKey: codableTypeKey, valueKey: valueKey)
} else if let error = error {
try encode(nsCodingUserData: error as NSError, key: valueKey)
}
}

private mutating func encode(nsCodingUserData: NSCoding, key: Key) throws {
let data: Data
if #available(iOS 11.0, *) {
data = try NSKeyedArchiver.archivedData(withRootObject: nsCodingUserData, requiringSecureCoding: false)
} else {
data = NSKeyedArchiver.archivedData(withRootObject: nsCodingUserData)
}
try encode(data, forKey: key)
}

private mutating func encode(codableUserData: Codable, typeKey: Key, valueKey: Key) throws {
guard let coder = Coders.getCoder(for: codableUserData) else {
throw SwedbankPaySDKController.StateRestorationError.unregisteredCodable(defaultEncodedTypeName(for: type(of: codableUserData)))
}
try encode(coder.encodedTypeName, forKey: typeKey)
try coder.encode(to: &self, key: valueKey, value: codableUserData)
}
}
extension KeyedDecodingContainer {
func decodeUserDataIfPresent(codableTypeKey: Key, valueKey: Key) throws -> Any? {
if let encodedTypeName = try decodeIfPresent(String.self, forKey: codableTypeKey) {
guard let coder = Coders.getCoder(for: encodedTypeName) else {
throw SwedbankPaySDKController.StateRestorationError.unregisteredCodable(encodedTypeName)
}
return try coder.decode(from: self, key: valueKey)
} else {
let data = try decodeIfPresent(Data.self, forKey: valueKey)
return try data.flatMap(NSKeyedUnarchiver.unarchiveTopLevelObjectWithData)
}
}

func decodeErrorIfPresent(codableTypeKey: Key, valueKey: Key) throws -> Error? {
let error = try decodeUserDataIfPresent(codableTypeKey: codableTypeKey, valueKey: valueKey)
switch error {
case nil:
return nil
case let error as Error:
return error
default:
// This should never happen
throw SwedbankPaySDKController.StateRestorationError.unknown
}
}
}

private protocol ErasedCoder {
var encodedTypeName: String { get }
func encode<K: CodingKey>(to container: inout KeyedEncodingContainer<K>, key: K, value: Any) throws
func decode<K: CodingKey>(from container: KeyedDecodingContainer<K>, key: K) throws -> Any
}

private enum Coders {}
extension Coders {
private static var registeredCoders = CoderMap()
private static let internalCoders: CoderMap = {
var map = CoderMap()
map.registerInternalCoder(SwedbankPaySDKController.WebContentError.self)
map.registerInternalCoder(SwedbankPaySDKController.StateRestorationError.self)
return map
}()

static func registerCoder<T: Codable>(for type: T.Type, encodedTypeName: String) {
registeredCoders.registerCoder(for: type, encodedTypeName: encodedTypeName)
}
static func getCoder(for codable: Codable) -> ErasedCoder? {
let codableType = type(of: codable)
return registeredCoders[codableType] ?? internalCoders[codableType]
}
static func getCoder(for encodedTypeName: String) -> ErasedCoder? {
return registeredCoders[encodedTypeName] ?? internalCoders[encodedTypeName]
}
}

private struct CoderMap {
private var byType: [ObjectIdentifier: ErasedCoder] = [:]
private var byName: [String: ErasedCoder] = [:]

mutating func registerCoder<T: Codable>(for type: T.Type, encodedTypeName: String) {
let coder = TypedCoder<T>(encodedTypeName: encodedTypeName)
byType[ObjectIdentifier(type)] = coder
byName[encodedTypeName] = coder
}

subscript(type: Codable.Type) -> ErasedCoder? {
return byType[ObjectIdentifier(type)]
}
subscript(encodedTypeName: String) -> ErasedCoder? {
return byName[encodedTypeName]
}

private struct TypedCoder<T: Codable>: ErasedCoder {
let encodedTypeName: String
func encode<K: CodingKey>(to container: inout KeyedEncodingContainer<K>, key: K, value: Any) throws {
try container.encode(value as! T, forKey: key)
}
func decode<K: CodingKey>(from container: KeyedDecodingContainer<K>, key: K) throws -> Any {
try container.decode(T.self, forKey: key)
}
}
}

extension CoderMap {
mutating func registerInternalCoder<T: Codable>(_ type: T.Type) {
let encodedTypeName = "\(internalEncodedTypeNamePrefix)\(String(describing: type))"
registerCoder(for: type, encodedTypeName: encodedTypeName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public extension SwedbankPaySDK {
}

/// Consumer object for Swedbank Pay SDK
struct Consumer: Codable {
struct Consumer: Codable, Equatable {
public var operation: ConsumerOperation
public var language: Language
public var shippingAddressRestrictedToCountryCodes: [String]
Expand Down
Loading

0 comments on commit ac61321

Please sign in to comment.