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

Fix Retry After Previous Offline State. #247

Merged
merged 11 commits into from
Oct 25, 2024
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
# Release Notes

## [Unreleased]

### Changed
* Split up `submitJob()` functionalities for BiometricKYC for easier readability and debugging.
* Remove setting job processing screen sucess state subtitle with `errorMessageRes`.
* Modify how we check for network failure due to internet connection and move the `isNetworkFailure()` function into a more appropriate scope.

### Fixed
* Improve how we handle offline job failure scenario.

## 10.2.14

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ struct OrchestratedBiometricKycScreen: View {
for: "BiometricKYC.Success.Title"
),
successSubtitle: SmileIDResourcesHelper.localizedString(
for: $viewModel.errorMessageRes.wrappedValue ?? "BiometricKYC.Success.Subtitle"
for: "BiometricKYC.Success.Subtitle"
),
successIcon: SmileIDResourcesHelper.CheckBold,
errorTitle: SmileIDResourcesHelper.localizedString(for: "BiometricKYC.Error.Title"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,30 @@ internal enum BiometricKycStep {

internal class OrchestratedBiometricKycViewModel: ObservableObject {
// MARK: - Input Properties

private let userId: String
private let jobId: String
private let allowNewEnroll: Bool
private var extraPartnerParams: [String: String]
private let localMetadata = LocalMetadata()
private var idInfo: IdInfo

// MARK: - Other Properties

internal var selfieFile: URL?
internal var livenessFiles: [URL]?
private var error: Error?
private var didSubmitBiometricJob: Bool = false

// MARK: - UI Properties

/// we use `errorMessageRes` to map to the actual code to the stringRes to allow localization,
/// and use `errorMessage` to show the actual platform error message that we show if
/// `errorMessageRes` is not set by the partner
@Published var errorMessageRes: String?
@Published var errorMessage: String?
@Published @MainActor private(set) var step: BiometricKycStep = .selfie

init(
userId: String,
jobId: String,
Expand All @@ -45,19 +45,19 @@ internal class OrchestratedBiometricKycViewModel: ObservableObject {
self.idInfo = idInfo
self.extraPartnerParams = extraPartnerParams
}

func onRetry() {
if selfieFile != nil {
submitJob()
} else {
DispatchQueue.main.async { self.step = .selfie }
updateStep(.selfie)
}
}

func onFinished(delegate: BiometricKycResultDelegate) {
if let selfieFile = selfieFile,
let livenessFiles = livenessFiles,
let selfiePath = getRelativePath(from: selfieFile) {
let livenessFiles = livenessFiles,
let selfiePath = getRelativePath(from: selfieFile) {
delegate.didSucceed(
selfieImage: selfiePath,
livenessImages: livenessFiles.compactMap { getRelativePath(from: $0) },
Expand All @@ -69,130 +69,177 @@ internal class OrchestratedBiometricKycViewModel: ObservableObject {
delegate.didError(error: SmileIDError.unknown("onFinish with no result or error"))
}
}

func submitJob() {
DispatchQueue.main.async { self.step = .processing(.inProgress) }
updateStep(.processing(.inProgress))
Task {
do {
selfieFile = try LocalStorage.getFileByType(
jobId: jobId,
fileType: FileType.selfie
)

livenessFiles = try LocalStorage.getFilesByType(
jobId: jobId,
fileType: FileType.liveness
)

guard let selfieFile else {
// Set step to .selfieCapture so that the Retry button goes back to this step
DispatchQueue.main.async { self.step = .selfie }
error = SmileIDError.unknown("Error capturing selfie")
return
}

var allFiles = [URL]()
let infoJson = try LocalStorage.createInfoJsonFile(
jobId: jobId,
idInfo: idInfo.copy(entered: true),
selfie: selfieFile,
livenessImages: livenessFiles
)
allFiles.append(contentsOf: [selfieFile, infoJson])
if let livenessFiles {
allFiles.append(contentsOf: livenessFiles)
}
let zipData = try LocalStorage.zipFiles(at: allFiles)
let authRequest = AuthenticationRequest(
jobType: .biometricKyc,
enrollment: false,
jobId: jobId,
userId: userId,
country: idInfo.country,
idType: idInfo.idType
)
if SmileID.allowOfflineMode {
try LocalStorage.saveOfflineJob(
jobId: jobId,
userId: userId,
jobType: .biometricKyc,
enrollment: false,
allowNewEnroll: allowNewEnroll,
localMetadata: localMetadata,
partnerParams: extraPartnerParams
)
}
let authResponse = try await SmileID.api.authenticate(request: authRequest)
let prepUploadRequest = PrepUploadRequest(
partnerParams: authResponse.partnerParams.copy(extras: extraPartnerParams),
allowNewEnroll: String(allowNewEnroll), // TODO: - Fix when Michael changes this to boolean
metadata: localMetadata.metadata.items,
timestamp: authResponse.timestamp,
signature: authResponse.signature
)
let prepUploadResponse: PrepUploadResponse
do {
prepUploadResponse = try await SmileID.api.prepUpload(
request: prepUploadRequest
)
} catch let error as SmileIDError {
switch error {
case .api("2215", _):
prepUploadResponse = try await SmileID.api.prepUpload(
request: prepUploadRequest.copy(retry: "true")
)
default:
throw error
}
}
let _ = try await SmileID.api.upload(
zip: zipData,
to: prepUploadResponse.uploadUrl
)
didSubmitBiometricJob = true
do {
try LocalStorage.moveToSubmittedJobs(jobId: self.jobId)
} catch {
print("Error moving job to submitted directory: \(error)")
self.error = error
DispatchQueue.main.async { self.step = .processing(.error) }
return
}
DispatchQueue.main.async { self.step = .processing(.success) }
try await handleJobSubmission()
updateStep(.processing(.success))
} catch let error as SmileIDError {
do {
_ = try LocalStorage.handleOfflineJobFailure(
jobId: self.jobId,
error: error
)
} catch {
print("Error moving job to submitted directory: \(error)")
self.error = error
return
}
if SmileID.allowOfflineMode, LocalStorage.isNetworkFailure(error: error) {
didSubmitBiometricJob = true
DispatchQueue.main.async {
self.errorMessageRes = "Offline.Message"
self.step = .processing(.success)
}
} else {
didSubmitBiometricJob = false
print("Error submitting job: \(error)")
let (errorMessageRes, errorMessage) = toErrorMessage(error: error)
self.error = error
DispatchQueue.main.async {
self.errorMessageRes = errorMessageRes
self.errorMessage = errorMessage
self.step = .processing(.error)
}
}
handleSubmissionFailure(error)
} catch {
didSubmitBiometricJob = false
print("Error submitting job: \(error)")
self.error = error
DispatchQueue.main.async { self.step = .processing(.error) }
updateStep(.processing(.error))
}
}
}

private func handleJobSubmission() async throws {
try fetchRequiredFiles()

let zipData = try createZipData()

let authResponse = try await authenticate()

let preUploadResponse = try await prepareForUpload(authResponse: authResponse)

try await uploadFiles(zipData: zipData, uploadUrl: preUploadResponse.uploadUrl)
didSubmitBiometricJob = true

try moveJobToSubmittedDirectory()
}

private func fetchRequiredFiles() throws {
selfieFile = try LocalStorage.getFileByType(
jobId: jobId,
fileType: FileType.selfie
)

livenessFiles = try LocalStorage.getFilesByType(
jobId: jobId,
fileType: FileType.liveness
)

guard selfieFile != nil else {
// Set step to .selfieCapture so that the Retry button goes back to this step
updateStep(.selfie)
error = SmileIDError.unknown("Error capturing selfie")
return
}
}

private func createZipData() throws -> Data {
var allFiles = [URL]()
let infoJson = try LocalStorage.createInfoJsonFile(
jobId: jobId,
idInfo: idInfo.copy(entered: true),
selfie: selfieFile,
livenessImages: livenessFiles
)
if let selfieFile {
allFiles.append(contentsOf: [selfieFile, infoJson])
}
if let livenessFiles {
allFiles.append(contentsOf: livenessFiles)
}
return try LocalStorage.zipFiles(at: allFiles)
}

private func authenticate() async throws -> AuthenticationResponse {
let authRequest = AuthenticationRequest(
jobType: .biometricKyc,
enrollment: false,
jobId: jobId,
userId: userId,
country: idInfo.country,
idType: idInfo.idType
)

if SmileID.allowOfflineMode {
try saveOfflineJobIfAllowed()
}

return try await SmileID.api.authenticate(request: authRequest)
}

private func saveOfflineJobIfAllowed() throws {
try LocalStorage.saveOfflineJob(
jobId: jobId,
userId: userId,
jobType: .biometricKyc,
enrollment: false,
allowNewEnroll: allowNewEnroll,
localMetadata: localMetadata,
partnerParams: extraPartnerParams
)
}

private func prepareForUpload(authResponse: AuthenticationResponse) async throws -> PrepUploadResponse {
let prepUploadRequest = PrepUploadRequest(
partnerParams: authResponse.partnerParams.copy(extras: extraPartnerParams),
allowNewEnroll: String(allowNewEnroll), // TODO: - Fix when Michael changes this to boolean
metadata: localMetadata.metadata.items,
timestamp: authResponse.timestamp,
signature: authResponse.signature
)
do {
return try await SmileID.api.prepUpload(
request: prepUploadRequest
)
} catch let error as SmileIDError {
guard case let .api(errorCode, _) = error,
errorCode == "2215"
else {
throw error
}
return try await SmileID.api.prepUpload(
request: prepUploadRequest.copy(retry: "true"))
}
}

private func uploadFiles(zipData: Data, uploadUrl: String) async throws {
try await SmileID.api.upload(
zip: zipData,
to: uploadUrl
)
}

private func moveJobToSubmittedDirectory() throws {
try LocalStorage.moveToSubmittedJobs(jobId: self.jobId)
}

private func updateStep(_ newStep: BiometricKycStep) {
DispatchQueue.main.async {
self.step = newStep
}
}

private func updateErrorMessages(
errorMessage: String? = nil,
errorMessageRes: String? = nil
) {
DispatchQueue.main.async {
self.errorMessage = errorMessage
self.errorMessageRes = errorMessageRes
}
}

private func handleSubmissionFailure(_ smileIDError: SmileIDError) {
do {
_ = try LocalStorage.handleOfflineJobFailure(
jobId: self.jobId,
error: smileIDError
)
} catch {
print("Error moving job to submitted directory: \(error)")
self.error = smileIDError
return
}

if SmileID.allowOfflineMode, SmileIDError.isNetworkFailure(error: smileIDError) {
didSubmitBiometricJob = true
updateErrorMessages(errorMessageRes: "Offline.Message")
updateStep(.processing(.success))
} else {
didSubmitBiometricJob = false
print("Error submitting job: \(smileIDError)")
let (errorMessageRes, errorMessage) = toErrorMessage(error: smileIDError)
self.error = smileIDError
updateErrorMessages(errorMessage: errorMessage, errorMessageRes: errorMessageRes)
updateStep(.processing(.error))
}
}
}
Expand All @@ -205,9 +252,9 @@ extension OrchestratedBiometricKycViewModel: SmartSelfieResultDelegate {
) {
submitJob()
}

func didError(error _: Error) {
error = SmileIDError.unknown("Error capturing selfie")
DispatchQueue.main.async { self.step = .processing(.error) }
updateStep(.processing(.error))
}
}
Loading
Loading