diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index 785d03a4a..0fa3c6fb8 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -268,6 +268,7 @@ 755D187929A6A6F80009F5E8 /* WelcomeStyle.FilePickerButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755D187829A6A6F80009F5E8 /* WelcomeStyle.FilePickerButtonStyle.swift */; }; 755D187B29A6A7180009F5E8 /* WelcomeStyle.TitleImageStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755D187A29A6A7180009F5E8 /* WelcomeStyle.TitleImageStyle.swift */; }; 755D187F29A6B1B90009F5E8 /* Glia+StartEngagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755D187E29A6B1B90009F5E8 /* Glia+StartEngagement.swift */; }; + 75617DFF2B9F899B002E403D /* GliaTests+DirectId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75617DFE2B9F899B002E403D /* GliaTests+DirectId.swift */; }; 756979C52A1E5E3C002ED254 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 756979C42A1E5E3C002ED254 /* Assets.xcassets */; }; 756B8B1C2996B116001D2BB2 /* ChatCoordinator.Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 756B8B1B2996B116001D2BB2 /* ChatCoordinator.Environment.swift */; }; 756B8B1E2996BAEA001D2BB2 /* HeaderButton.Props.swift in Sources */ = {isa = PBXBuildFile; fileRef = 756B8B1D2996BAEA001D2BB2 /* HeaderButton.Props.swift */; }; @@ -1193,6 +1194,7 @@ 755D187829A6A6F80009F5E8 /* WelcomeStyle.FilePickerButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStyle.FilePickerButtonStyle.swift; sourceTree = ""; }; 755D187A29A6A7180009F5E8 /* WelcomeStyle.TitleImageStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStyle.TitleImageStyle.swift; sourceTree = ""; }; 755D187E29A6B1B90009F5E8 /* Glia+StartEngagement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Glia+StartEngagement.swift"; sourceTree = ""; }; + 75617DFE2B9F899B002E403D /* GliaTests+DirectId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GliaTests+DirectId.swift"; sourceTree = ""; }; 756979C42A1E5E3C002ED254 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 756B8B1B2996B116001D2BB2 /* ChatCoordinator.Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCoordinator.Environment.swift; sourceTree = ""; }; 756B8B1D2996BAEA001D2BB2 /* HeaderButton.Props.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderButton.Props.swift; sourceTree = ""; }; @@ -3801,6 +3803,7 @@ children = ( 8491AF612AA20F8F00CC3E72 /* Mocks */, 7512A5A627C3926500319DF1 /* GliaTests.swift */, + 75617DFE2B9F899B002E403D /* GliaTests+DirectId.swift */, 846A5C4429F6BEFA0049B29F /* GliaTests+StartEngagement.swift */, ); path = Glia; @@ -5967,6 +5970,7 @@ AF9DB23128905A1D00A0C442 /* ViewFactory.Environment.Failing.swift in Sources */, 9A3E1DA027BA7B9F005634EB /* FileSystemStorageTests.swift in Sources */, EB9ADB5A2829089F00FAE8A4 /* ChatItem+Equatable.swift in Sources */, + 75617DFF2B9F899B002E403D /* GliaTests+DirectId.swift in Sources */, EB7A150A286D98270035AC62 /* FileUploaderTests.swift in Sources */, EB27E71D27FEBB620090B895 /* CallViewModelTests.swift in Sources */, 846A5C3E29D1C7B00049B29F /* CallVisualizerTests.swift in Sources */, diff --git a/GliaWidgets/Public/Glia/Glia+StartEngagement.swift b/GliaWidgets/Public/Glia/Glia+StartEngagement.swift index 69fc84bae..ffd697b8c 100644 --- a/GliaWidgets/Public/Glia/Glia+StartEngagement.swift +++ b/GliaWidgets/Public/Glia/Glia+StartEngagement.swift @@ -172,7 +172,12 @@ extension Glia { log: loggerPhase.logger, snackBar: environment.snackBar, operatorRequestHandlerService: operatorRequestHandlerService, - maximumUploads: { self.maximumUploads } + maximumUploads: { self.maximumUploads }, + reloadAllChild: { coordinators in + coordinators.compactMap { $0 as? ChatCoordinator } + .first? + .reloadChatTranscript() + } ) ) rootCoordinator?.delegate = { [weak self] event in diff --git a/GliaWidgets/Public/Glia/Glia.OpaqueAuthentication.swift b/GliaWidgets/Public/Glia/Glia.OpaqueAuthentication.swift index 3d174f6c2..1ae3db393 100644 --- a/GliaWidgets/Public/Glia/Glia.OpaqueAuthentication.swift +++ b/GliaWidgets/Public/Glia/Glia.OpaqueAuthentication.swift @@ -16,6 +16,8 @@ extension Glia.Authentication { public enum Behavior { /// Restrict authentication and deauthentication during ongoing engagement. case forbiddenDuringEngagement + + case allowedDuringEngagement } } @@ -24,6 +26,8 @@ extension Glia.Authentication.Behavior { switch behavior { case .forbiddenDuringEngagement: self = .forbiddenDuringEngagement + case .allowedDuringEngagement: + self = .allowedDuringEngagement @unknown default: self = .forbiddenDuringEngagement } @@ -33,6 +37,8 @@ extension Glia.Authentication.Behavior { switch self { case .forbiddenDuringEngagement: return .forbiddenDuringEngagement + case .allowedDuringEngagement: + return .allowedDuringEngagement } } } @@ -59,12 +65,18 @@ extension Glia { public func authentication(with behavior: Glia.Authentication.Behavior) throws -> Authentication { let auth = try environment.coreSdk.authentication(behavior.toCoreSdk()) - // Reset navigation and UI back to initial state, - // effectively removing bubble view (if there was one). - let cleanup = { [weak self] in - self?.rootCoordinator?.popCoordinator() - self?.rootCoordinator?.end() - self?.rootCoordinator = nil + let completion = { [weak self] in + self?.environment.gcd.mainQueue.asyncAfterDeadline(.now() + .seconds(2)) { [weak self] in + if self?.interactor?.currentEngagement?.restartedFromEngagementId != nil { + self?.rootCoordinator?.reload() + } else { + // Reset navigation and UI back to initial state, + // effectively removing bubble view (if there was one). + self?.rootCoordinator?.popCoordinator() + self?.rootCoordinator?.end() + self?.rootCoordinator = nil + } + } } return .init( @@ -78,8 +90,8 @@ extension Glia { ) { result in switch result { case .success: - // Cleanup navigation and views. - cleanup() + // Handle authentication + completion() case .failure: break @@ -94,7 +106,7 @@ extension Glia { switch result { case .success: // Cleanup navigation and views. - cleanup() + completion() case .failure: break } diff --git a/GliaWidgets/Sources/Coordinators/Chat/ChatCoordinator.swift b/GliaWidgets/Sources/Coordinators/Chat/ChatCoordinator.swift index 700a964a8..f0d6bef08 100644 --- a/GliaWidgets/Sources/Coordinators/Chat/ChatCoordinator.swift +++ b/GliaWidgets/Sources/Coordinators/Chat/ChatCoordinator.swift @@ -64,6 +64,10 @@ class ChatCoordinator: SubFlowCoordinator, FlowCoordinator { return viewController } + func reloadChatTranscript() { + controller?.viewModel.event(.viewDidLoad) + } + private func makeChatViewController() -> ChatViewController { // We need to defer passing controller to transcript model, // because model will use it later, however controller diff --git a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.Mock.swift b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.Mock.swift index a3c8ffb5c..409bd2a40 100644 --- a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.Mock.swift +++ b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.Mock.swift @@ -39,7 +39,8 @@ extension EngagementCoordinator.Environment { log: .mock, snackBar: .mock, operatorRequestHandlerService: .mock(), - maximumUploads: { 2 } + maximumUploads: { 2 }, + reloadAllChild: { _ in } ) } #endif diff --git a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.swift b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.swift index 142ab48ee..aa999507d 100644 --- a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.swift +++ b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.Environment.swift @@ -41,5 +41,6 @@ extension EngagementCoordinator { var snackBar: SnackBar var operatorRequestHandlerService: OperatorRequestHandlerService var maximumUploads: () -> Int + var reloadAllChild: ([any FlowCoordinator]) -> Void } } diff --git a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.swift b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.swift index 28531fe5e..b30cb0918 100644 --- a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.swift +++ b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.swift @@ -127,6 +127,10 @@ class EngagementCoordinator: SubFlowCoordinator, FlowCoordinator { } // swiftlint:enable function_body_length + func reload() { + environment.reloadAllChild(coordinators) + } + deinit { print("\(Self.self) is deallocated.") } diff --git a/GliaWidgets/Sources/CoreSDKClient/CoreSDKClient.Mock.swift b/GliaWidgets/Sources/CoreSDKClient/CoreSDKClient.Mock.swift index b42311a71..1fb1b93e7 100644 --- a/GliaWidgets/Sources/CoreSDKClient/CoreSDKClient.Mock.swift +++ b/GliaWidgets/Sources/CoreSDKClient/CoreSDKClient.Mock.swift @@ -483,13 +483,15 @@ extension CoreSdkClient.Engagement { id: String = "", engagedOperator: Operator? = nil, source: EngagementSource = .coreEngagement, - fetchSurvey: @escaping FetchSurvey = { _, _ in }) - -> CoreSdkClient.Engagement { + fetchSurvey: @escaping FetchSurvey = { _, _ in }, + restartedFromEngagementId: String? = nil + ) -> CoreSdkClient.Engagement { .init( id: id, engagedOperator: engagedOperator, source: source, - fetchSurvey: fetchSurvey + fetchSurvey: fetchSurvey, + restartedFromEngagementId: restartedFromEngagementId ) } } diff --git a/GliaWidgetsTests/Coordinator/RootCoordinator.Environment.Failing.swift b/GliaWidgetsTests/Coordinator/RootCoordinator.Environment.Failing.swift index e82c94a9c..778e19870 100644 --- a/GliaWidgetsTests/Coordinator/RootCoordinator.Environment.Failing.swift +++ b/GliaWidgetsTests/Coordinator/RootCoordinator.Environment.Failing.swift @@ -93,6 +93,9 @@ extension EngagementCoordinator.Environment { maximumUploads: { fail("\(Self.self).maximumUploads") return 2 + }, + reloadAllChild: { _ in + fail("\(Self.self).reloadAllChild") } ) } diff --git a/GliaWidgetsTests/Sources/Glia/GliaTests+DirectId.swift b/GliaWidgetsTests/Sources/Glia/GliaTests+DirectId.swift new file mode 100644 index 000000000..b1d15d382 --- /dev/null +++ b/GliaWidgetsTests/Sources/Glia/GliaTests+DirectId.swift @@ -0,0 +1,99 @@ +import XCTest + +@testable import GliaWidgets + +final class GliaTestsDirectId: XCTestCase { + /// If ongoing engagement exists, it should be restarted after autentication. + /// To verify which should ongoing engagement restored SDK relyies on + /// `restartedEngagementId` property in `Engagement` instance. + func testAuthentication__shouldRestartEngagementAndFetchHistory() throws { + enum Call { case reloadAllChild } + + var calls = [Call]() + var authenticationResult: Result? + + var env = Glia.Environment.failing + env.coreSdk.createLogger = { _ in .mock } + env.gcd.mainQueue.asyncIfNeeded = { $0() } + env.gcd.mainQueue.asyncAfterDeadline = { $1() } + env.coreSdk.authentication = { _ in + CoreSdkClient.Authentication(authenticateWithIdToken: { $2(.success(())) }) + } + env.coreSDKConfigurator.configureWithInteractor = { _ in } + env.coreSDKConfigurator.configureWithConfiguration = { $1(.success(())) } + env.createRootCoordinator = { _, _, _, _, _, _, _ in + var coordinatorEnv = EngagementCoordinator.Environment.mock + coordinatorEnv.reloadAllChild = { _ in calls.append(.reloadAllChild) } + return .mock(environment: coordinatorEnv) + } + env.coreSdk.localeProvider = .mock + env.conditionalCompilation = .mock + + // Use case + let sdk = Glia(environment: env) + try sdk.configure(with: .mock(), theme: .mock()) { _ in } + try sdk.startEngagement(engagementKind: .chat, in: ["queue-id"]) + + sdk.interactor?.currentEngagement = .mock(restartedFromEngagementId: "restarted-id") + XCTAssertNotNil(sdk.rootCoordinator) + + let authentication = try sdk.authentication(with: .allowedDuringEngagement) + authentication.authenticate(with: "", accessToken: nil) { result in + authenticationResult = result + } + + XCTAssertEqual(authenticationResult?.map { true }, .some(.success(true))) + XCTAssertEqual(calls, [.reloadAllChild]) + XCTAssertNotNil(sdk.rootCoordinator) + } + + /// If after authentication a visitor has no ongoing engagement or engagement without + /// `restartedEngagementId` property in `Engagement` instance it should not be restored + /// automatically. + func testAuthentication__shouldNotSstartOngoingEngagementAfterAuthentication() throws { + enum Call { case reloadAllChild } + + var calls = [Call]() + var authenticationResult: Result? + + var env = Glia.Environment.failing + env.coreSdk.createLogger = { _ in .mock } + env.gcd.mainQueue.asyncIfNeeded = { $0() } + env.gcd.mainQueue.asyncAfterDeadline = { $1() } + env.coreSdk.authentication = { _ in + CoreSdkClient.Authentication(authenticateWithIdToken: { $2(.success(())) }) + } + env.coreSDKConfigurator.configureWithInteractor = { _ in } + env.coreSDKConfigurator.configureWithConfiguration = { $1(.success(())) } + env.createRootCoordinator = { _, _, _, _, _, _, _ in + var coordinatorEnv = EngagementCoordinator.Environment.mock + coordinatorEnv.reloadAllChild = { _ in calls.append(.reloadAllChild) } + return .mock(environment: coordinatorEnv) + } + env.coreSdk.localeProvider = .mock + env.conditionalCompilation = .mock + + // Use case + let sdk = Glia(environment: env) + try sdk.configure(with: .mock(), theme: .mock()) { _ in } + try sdk.startEngagement(engagementKind: .chat, in: ["queue-id"]) + + sdk.interactor?.currentEngagement = .mock() + XCTAssertNotNil(sdk.rootCoordinator) + + let authentication = try sdk.authentication(with: .allowedDuringEngagement) + authentication.authenticate(with: "", accessToken: nil) { result in + authenticationResult = result + } + + XCTAssertEqual(authenticationResult?.map { true }, .some(.success(true))) + XCTAssertEqual(calls, []) + XCTAssertNil(sdk.rootCoordinator) + } +} + +extension Glia.Authentication.Error: Equatable { + public static func == (lhs: Glia.Authentication.Error, rhs: Glia.Authentication.Error) -> Bool { + lhs.reason == rhs.reason + } +}