Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework model-service interactions #6

Merged
merged 6 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
04345A442B209A30006B40CC /* ButtonProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04345A432B209A30006B40CC /* ButtonProgressView.swift */; };
04345A472B20DD0C006B40CC /* Formatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04345A462B20DD0C006B40CC /* Formatters.swift */; };
04345A4A2B21D6F1006B40CC /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04345A492B21D6F1006B40CC /* Appearance.swift */; };
043701CF2B88A9330019696B /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043701CE2B88A9330019696B /* Constants.swift */; };
04420E752B11005000EA8790 /* TextDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04420E742B11005000EA8790 /* TextDivider.swift */; };
04420E772B110DD100EA8790 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 04420E762B110DD100EA8790 /* Localizable.xcstrings */; };
04420E792B14A84500EA8790 /* ExampleAppLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04420E782B14A84500EA8790 /* ExampleAppLogger.swift */; };
Expand Down Expand Up @@ -79,6 +80,7 @@
04345A432B209A30006B40CC /* ButtonProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonProgressView.swift; sourceTree = "<group>"; };
04345A462B20DD0C006B40CC /* Formatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Formatters.swift; sourceTree = "<group>"; };
04345A492B21D6F1006B40CC /* Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = "<group>"; };
043701CE2B88A9330019696B /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
04420E742B11005000EA8790 /* TextDivider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextDivider.swift; sourceTree = "<group>"; };
04420E762B110DD100EA8790 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
04420E782B14A84500EA8790 /* ExampleAppLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAppLogger.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -149,6 +151,7 @@
children = (
04345A462B20DD0C006B40CC /* Formatters.swift */,
04345A492B21D6F1006B40CC /* Appearance.swift */,
043701CE2B88A9330019696B /* Constants.swift */,
);
path = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -463,6 +466,7 @@
042391112AF3B17F00833025 /* LaunchScreenViewController.swift in Sources */,
04D781562AF517BD00A3B29B /* ImageColorInverterModifier.swift in Sources */,
04FAF9FA2AE81563002E4BAE /* ExampleApp.swift in Sources */,
043701CF2B88A9330019696B /* Constants.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
17 changes: 7 additions & 10 deletions Co-Badged Cards/SwiftUI/Co-Badged Cards Example/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,15 @@ import SwiftUI

struct ContentView: View {

let service: PrimerDataService

@StateObject var settingsModel: SettingsModel = .init()

@StateObject var settingsModel: SettingsModel

init(service: PrimerDataService) {
_settingsModel = StateObject(wrappedValue: SettingsModel(service: service))
}

var body: some View {
StartPage(service: service, settingsModel: settingsModel)
StartPage(settingsModel: settingsModel)
.navigationTitle("App.Title")
.navigationBarTitleDisplayMode(.inline)
.onReceive(settingsModel.$clientToken, perform: onReceive(clientToken:))
}

func onReceive(clientToken: String) {
service.clientToken = clientToken
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct ExampleApp: App {
static var clientToken = ""

// 👇 You can point to a server that provides the client token here
static var clientTokenUrl = ""
static var clientTokenUrl = "https://my.glitch.server/"

let service = PrimerDataService(clientToken: Self.clientToken)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import PrimerSDK

class PrimerCardDataErrorsModel: PrimerBaseCardDataModel {

override init() {
init(service: PrimerDataService) {
super.init()
logger.info("[PrimerCardDataErrorsModel.init]")
service.errorsDelegate = self
}

fileprivate func clearErrors() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,31 +47,38 @@ class PrimerCardDataModel: PrimerBaseCardDataModel {
var shouldDisplayCardSelectionView: Bool {
return !cardNumber.isEmpty && !cardNetworksModel.cardNetworks.isEmpty
}

let service: PrimerDataService

init(service: PrimerDataService) {
self.service = service
super.init()
objectWillChange.sink {
DispatchQueue.main.async {
self.service.update(withModel: self)
}
}.store(in: &cancellables)
service.modelsDelegate = self
}

weak var service: PrimerDataService?

func makePayment(_ completion: @escaping (PaymentResultModel) -> Void) {
service.makePayment { result in
completion(result)
}
}

func updateCardNetworks(with networks: [CardDisplayModel]) {
cardNetworksModel.cardNetworks = networks
if !networks.isEmpty {
selectCardNetwork(at: 0)
}
}

func selectCardNetwork(at index: Int) {
selectedCardNetwork = cardNetworksModel.cardNetworks[index].value
objectWillChange.send()
}

override init() {
super.init()
logger.info("[PrimerCardDataModel.init]")
objectWillChange.sink {
DispatchQueue.main.async {
self.service?.update(withModel: self)
}
}.store(in: &cancellables)
}


var isEmpty: Bool {
[cardNumber, expiryDate, cvvNumber, cardholderName].allSatisfy { $0.isEmpty }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class SettingsModel: ObservableObject {
case clientTokenUrl = "CLIENT_TOKEN_URL"
}

let service: PrimerDataService

@Published var clientToken: String = ""

@Published var clientTokenUrl: String = "" {
Expand All @@ -27,7 +29,9 @@ class SettingsModel: ObservableObject {

@Published var fetchErrorMessage: String? = nil

init() {
init(service: PrimerDataService) {
self.service = service

if let clientToken = UserDefaults.standard.string(forKey: Key.clientToken.rawValue) {
self.clientToken = clientToken
} else if !ExampleApp.clientToken.isEmpty {
Expand All @@ -38,13 +42,51 @@ class SettingsModel: ObservableObject {
} else {
self.clientTokenUrl = ExampleApp.clientTokenUrl
}

}

var isClientTokenValid: Bool {
return !clientToken.isEmpty && isValidJWT(clientToken)
}


func updateClientToken() async throws {
DispatchQueue.main.sync {
fetchErrorMessage = nil
}
do {
let clientToken = try await service.fetchClientToken(from: clientTokenUrl)
DispatchQueue.main.sync {
self.clientToken = clientToken
}
} catch {
DispatchQueue.main.sync {
fetchErrorMessage = ErrorMessages.clientTokenFetch(clientTokenUrl: clientTokenUrl)
clientToken = ""
}
throw error
}
}

func setup() async throws {
if !isClientTokenValid {
try await updateClientToken()
}

do {
try await service.start()
service.configureForPayments()
} catch {
logger.error(error.localizedDescription)
fetchErrorMessage = ErrorMessages.sdkStart
clientToken = ""

if let error = error as? PrimerDataService.Error {
logger.error(error.message)
}

throw error
}
}

// MARK: Helpers

var isConfiguredForMakingPayment: Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ class PrimerDataService: NSObject {
case failedToFetchClientToken(error: Swift.Error)
case failedToInitialiseSDK(error: Swift.Error)
case paymentFailed(error: Swift.Error)

var message: String {
switch self {
case .failedToInitialiseSDK(let error),
.failedToFetchClientToken(let error),
.paymentFailed(let error):
return error.localizedDescription
}
}
}

var clientToken: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import UIKit

final class Appearance {

static func setup() {
UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.white]
UINavigationBar.appearance().tintColor = .white
Expand All @@ -17,5 +16,4 @@ final class Appearance {
UINavigationBar.appearance().isTranslucent = false
UIBarButtonItem.appearance().tintColor = .white
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// Constants.swift
// Co-Badged Cards Example
//
// Created by Jack Newcombe on 23/02/2024.
//

import Foundation

struct ErrorMessages {
private init() {}

static let sdkStart = "There was an error starting the SDK - check your configuration."

static func clientTokenFetch(clientTokenUrl: String) -> String {
"""
Could not fetch a client token from:
POST \(clientTokenUrl)
Make sure the server is running and that your network connection is working.
"""
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ import Combine

struct CardFormFullPageView: View {

let service: PrimerDataService
let model: PrimerCardDataModel

var model: PrimerCardDataModel = .init()

@StateObject var errorsModel: PrimerCardDataErrorsModel = .init()
@StateObject var errorsModel: PrimerCardDataErrorsModel

var cancellables: Set<AnyCancellable> = []

@State var paymentModel: PaymentResultModel?


init(model: PrimerCardDataModel, errorsModel: PrimerCardDataErrorsModel) {
self.model = model
self._errorsModel = StateObject(wrappedValue: errorsModel)
}

var body: some View {
VStack(spacing: 12) {
Image("primer-icon")
Expand All @@ -36,13 +39,10 @@ struct CardFormFullPageView: View {
}

func onAppear() {
self.model.service = service
self.service.errorsDelegate = errorsModel
self.service.modelsDelegate = model
}

func onSubmit(_ completion: @escaping () -> Void) {
service.makePayment { result in
model.makePayment { result in
self.paymentModel = result
completion()
}
Expand All @@ -51,7 +51,6 @@ struct CardFormFullPageView: View {
}

#Preview {
CardFormFullPageView(service: .init(clientToken: ""),
model: .init(),
errorsModel: .init())
CardFormFullPageView(model: .init(service: .init(clientToken: "")),
errorsModel: .init(service: .init(clientToken: "")))
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import SwiftUI

struct SettingsView: View {

let service: PrimerDataService

@StateObject var settingsModel: SettingsModel

@State var isFetchingClientToken: Bool = false
Expand Down Expand Up @@ -74,24 +72,10 @@ struct SettingsView: View {

private func onFetchClientToken() {
Task {
settingsModel.fetchErrorMessage = nil
isFetchingClientToken = true
defer { isFetchingClientToken = false }

defer {
isFetchingClientToken = false
}

do {
settingsModel.clientToken = try await service.fetchClientToken(from: settingsModel.clientTokenUrl)
} catch {
settingsModel.fetchErrorMessage = """
Could not fetch a client token from:
POST \(settingsModel.clientTokenUrl)
Make sure the server is running and that your network connection is working.
"""
settingsModel.clientToken = ""
throw error
}
try await settingsModel.updateClientToken()
}
}

Expand All @@ -104,6 +88,6 @@ Make sure the server is running and that your network connection is working.

#Preview {
Form {
SettingsView(service: .init(clientToken: ""), settingsModel: .init())
SettingsView(settingsModel: .init(service: .init(clientToken: "")))
}
}
Loading
Loading