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

Entry widget and secure conversations v2 #1036

Draft
wants to merge 36 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
555c40d
Add EntryWidget UI
rasmustautsglia Sep 23, 2024
0cc4536
Add Engagement Launcher interface and getter from Glia
ykyivskyi-gl Sep 25, 2024
0e0419a
Add EntryWidget embedded view
rasmustautsglia Sep 26, 2024
96c6629
Implement EngagementLauncher logic
ykyivskyi-gl Sep 30, 2024
71d4f0f
Add Unified Customization to EntryWidget
rasmustautsglia Oct 3, 2024
e35dd27
Update Entry Widget media types texts
rasmustautsglia Oct 8, 2024
7df802e
Implement QueuesMonitor to observe queues' engagement type updates
ykyivskyi-gl Oct 8, 2024
f7d9f00
Add remote configuration for secure conversations
igorkravchenko Oct 11, 2024
8209276
Integrate queues monitor and engagement launcher into EntryWidget
ykyivskyi-gl Oct 13, 2024
f20858a
Add new localization keys and update existing for local strings for SC
igorkravchenko Oct 14, 2024
3ed154a
Make entry widget height dynamic
rasmustautsglia Oct 16, 2024
9881e0a
Update 'Check Messages' button and margins on SC Welcome screen
igorkravchenko Oct 15, 2024
a429245
Add new public interfaces to the Entry Widget
rasmustautsglia Oct 18, 2024
0f37e41
Cover QueuesMonitor with unit tests
ykyivskyi-gl Oct 18, 2024
7c13ce6
Add new localizations for the Entry Widget.
AndriiHorishniiMOC Oct 16, 2024
0daa650
Add Entry Widget Loading view design
rasmustautsglia Oct 21, 2024
3c767ad
Introduce SC bottom banner UI on chat screen
igorkravchenko Oct 21, 2024
9d1e623
Implement unified customization for SC bottom banner
igorkravchenko Oct 23, 2024
39b50fb
Make passed getEngagementLauncher queuesId paramater non-nullable
ykyivskyi-gl Oct 24, 2024
dfcdc2f
Leave Current Conversation dialog UI
Oct 25, 2024
eff26b3
Remote config for Leave Current Conversation dialog
Oct 25, 2024
f1bf9df
Hide the "Secure Messaging" if visitor is not authenticated
rasmustautsglia Oct 24, 2024
f03358d
Snapshot tests for Leave Current Conversation dialog
Oct 25, 2024
de8d53d
Add accessibility modifiers to the Entry Widget
AndriiHorishniiMOC Oct 22, 2024
ceb6549
Add new send message unavailability banner
igorkravchenko Oct 24, 2024
c797577
Add unified customization for new SC unavailability indicator for chat
igorkravchenko Oct 26, 2024
f74c4fc
Make EntryWidget sheet landscape adaptable
rasmustautsglia Oct 28, 2024
b3cd12d
Replace unavailability dialog with banner view on Transcript screen
igorkravchenko Oct 28, 2024
14d76ce
Toggle SC bottom banner visibility based on view model
igorkravchenko Oct 29, 2024
c16588b
Use queues from QueuesMonitor to determine Secure Conversations avail…
ykyivskyi-gl Oct 30, 2024
3d965bb
Improve the Entry Widget Style structures
AndriiHorishniiMOC Oct 28, 2024
0c749bd
Hide EntryWidget sheet before engagement
rasmustautsglia Nov 4, 2024
d92ca92
Add snapshot tests to EntryWidget
rasmustautsglia Nov 1, 2024
1ef7ef3
Remove rebasing issue
rasmustautsglia Nov 5, 2024
50f856e
Optimize using of strings for the Entry Widget
AndriiHorishniiMOC Nov 4, 2024
e829a5a
Introduce disabled state for ChatMessageEntry style
igorkravchenko Nov 2, 2024
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
290 changes: 274 additions & 16 deletions GliaWidgets.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions GliaWidgets/Asset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public enum Asset {
public static let uploadRemove = ImageAsset(name: "uploadRemove")
public static let chatPickMedia = ImageAsset(name: "chatPickMedia")
public static let chatSend = ImageAsset(name: "chatSend")
public static let sendMessageUnavailableInfo = ImageAsset(name: "send-message-unavailable-info")
public static let unreadMessageIndicator = ImageAsset(name: "unreadMessageIndicator")
public static let back = ImageAsset(name: "back")
public static let close = ImageAsset(name: "close")
Expand Down Expand Up @@ -109,6 +110,7 @@ public enum Asset {
uploadRemove,
chatPickMedia,
chatSend,
sendMessageUnavailableInfo,
unreadMessageIndicator,
back,
close,
Expand Down
124 changes: 112 additions & 12 deletions GliaWidgets/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -432,14 +432,88 @@ internal enum Localization {
internal static var message: String { Localization.tr("Localizable", "engagement.queue_wait.message", fallback: "You can continue browsing and we will connect you automatically.") }
}
internal enum SecureMessaging {
/// Messaging
internal static var title: String { Localization.tr("Localizable", "engagement.secure_messaging.title", fallback: "Messaging") }
/// Secure Messaging
internal static var title: String { Localization.tr("Localizable", "engagement.secure_messaging.title", fallback: "Secure Messaging") }
}
internal enum Video {
/// Video
internal static var title: String { Localization.tr("Localizable", "engagement.video.title", fallback: "Video") }
}
}
internal enum EntryWidget {
internal enum Audio {
internal enum Button {
/// Speak through your device
internal static var description: String { Localization.tr("Localizable", "entry_widget.audio.button.description", fallback: "Speak through your device") }
/// Audio
internal static var label: String { Localization.tr("Localizable", "entry_widget.audio.button.label", fallback: "Audio") }
internal enum Accessibility {
/// Starts a call
internal static var hint: String { Localization.tr("Localizable", "entry_widget.audio.button.accessibility.hint", fallback: "Starts a call") }
}
}
}
internal enum EmptyState {
/// We are here to assist you during our business hours.
internal static var description: String { Localization.tr("Localizable", "entry_widget.empty_state.description", fallback: "We are here to assist you during our business hours.") }
/// Support team is currently offline
internal static var title: String { Localization.tr("Localizable", "entry_widget.empty_state.title", fallback: "Support team is currently offline") }
}
internal enum ErrorState {
/// We could not load the contacts at this time. This may be due to a temporary syncing issue or network problem.
internal static var description: String { Localization.tr("Localizable", "entry_widget.error_state.description", fallback: "We could not load the contacts at this time. This may be due to a temporary syncing issue or network problem.") }
/// Could not load the contacts
internal static var title: String { Localization.tr("Localizable", "entry_widget.error_state.title", fallback: "Could not load the contacts") }
internal enum TryAgain {
internal enum Button {
/// Try again
internal static var label: String { Localization.tr("Localizable", "entry_widget.error_state.try_again.button.label", fallback: "Try again") }
}
}
}
internal enum LiveChat {
internal enum Button {
/// For the texter in all of us
internal static var description: String { Localization.tr("Localizable", "entry_widget.live_chat.button.description", fallback: "For the texter in all of us") }
/// Live Chat
internal static var label: String { Localization.tr("Localizable", "entry_widget.live_chat.button.label", fallback: "Live Chat") }
internal enum Accessibility {
/// Starts a chat
internal static var hint: String { Localization.tr("Localizable", "entry_widget.live_chat.button.accessibility.hint", fallback: "Starts a chat") }
}
}
}
internal enum Loading {
internal enum Accessibility {
/// Loading indicator. Waiting for available options.
internal static var label: String { Localization.tr("Localizable", "entry_widget.loading.accessibility.label", fallback: "Loading indicator. Waiting for available options.") }
}
}
internal enum SecureMessaging {
internal enum Button {
/// Start a conversation, we’ll get back to you
internal static var description: String { Localization.tr("Localizable", "entry_widget.secure_messaging.button.description", fallback: "Start a conversation, we’ll get back to you") }
/// Secure Messaging
internal static var label: String { Localization.tr("Localizable", "entry_widget.secure_messaging.button.label", fallback: "Secure Messaging") }
internal enum Accessibility {
/// Starts messaging with us
internal static var hint: String { Localization.tr("Localizable", "entry_widget.secure_messaging.button.accessibility.hint", fallback: "Starts messaging with us") }
}
}
}
internal enum Video {
internal enum Button {
/// Face-to-face, just like in person
internal static var description: String { Localization.tr("Localizable", "entry_widget.video.button.description", fallback: "Face-to-face, just like in person") }
/// Video
internal static var label: String { Localization.tr("Localizable", "entry_widget.video.button.label", fallback: "Video") }
internal enum Accessibility {
/// Starts a video call
internal static var hint: String { Localization.tr("Localizable", "entry_widget.video.button.accessibility.hint", fallback: "Starts a video call") }
}
}
}
}
internal enum Error {
/// Something went wrong.
internal static var general: String { Localization.tr("Localizable", "error.general", fallback: "Something went wrong.") }
Expand Down Expand Up @@ -563,11 +637,11 @@ internal enum Localization {
}
}
internal enum MessageCenter {
/// Messaging
internal static var header: String { Localization.tr("Localizable", "message_center.header", fallback: "Messaging") }
/// Secure Messaging
internal static var header: String { Localization.tr("Localizable", "message_center.header", fallback: "Secure Messaging") }
internal enum Confirmation {
/// Your message has been sent. We will get back to you within 48 hours.
internal static var subtitle: String { Localization.tr("Localizable", "message_center.confirmation.subtitle", fallback: "Your message has been sent. We will get back to you within 48 hours.") }
/// Your message has been sent. We will get back to you within 1 business day.
internal static var subtitle: String { Localization.tr("Localizable", "message_center.confirmation.subtitle", fallback: "Your message has been sent. We will get back to you within 1 business day.") }
internal enum CheckMessages {
internal enum Accessibility {
/// Navigates you to the chat transcript.
Expand All @@ -592,10 +666,10 @@ internal enum Localization {
internal static var checkMessages: String { Localization.tr("Localizable", "message_center.welcome.check_messages", fallback: "Check messages") }
/// Your message
internal static var messageTitle: String { Localization.tr("Localizable", "message_center.welcome.message_title", fallback: "Your message") }
/// Send a message and we will get back to you within 48 hours.
internal static var subtitle: String { Localization.tr("Localizable", "message_center.welcome.subtitle", fallback: "Send a message and we will get back to you within 48 hours.") }
/// Welcome to Message Center
internal static var title: String { Localization.tr("Localizable", "message_center.welcome.title", fallback: "Welcome to Message Center") }
/// Send a message and we will get back to you within 1 business day.
internal static var subtitle: String { Localization.tr("Localizable", "message_center.welcome.subtitle", fallback: "Send a message and we will get back to you within 1 business day.") }
/// Welcome to Secure Messaging
internal static var title: String { Localization.tr("Localizable", "message_center.welcome.title", fallback: "Welcome to Secure Messaging") }
internal enum CheckMessages {
internal enum Accessibility {
/// Navigates you to the chat transcript.
Expand All @@ -611,8 +685,8 @@ internal enum Localization {
}
}
internal enum MessageInput {
/// Enter your message
internal static var placeholder: String { Localization.tr("Localizable", "message_center.welcome.message_input.placeholder", fallback: "Enter your message") }
/// Enter message
internal static var placeholder: String { Localization.tr("Localizable", "message_center.welcome.message_input.placeholder", fallback: "Enter message") }
}
internal enum MessageLength {
/// The message cannot exceed 10,000 characters.
Expand Down Expand Up @@ -644,6 +718,32 @@ internal enum Localization {
}
}
}
internal enum SecureMessaging {
internal enum Chat {
internal enum Banner {
/// Secure messaging has an expected response time of 1 business day.
internal static var bottom: String { Localization.tr("Localizable", "secure_messaging.chat.banner.bottom", fallback: "Secure messaging has an expected response time of 1 business day.") }
/// Need live support?
internal static var top: String { Localization.tr("Localizable", "secure_messaging.chat.banner.top", fallback: "Need live support?") }
}
internal enum LeaveCurrentConversation {
/// You have an ongoing conversation. Starting a new conversation before ongoing ones are resolved may lead to our agents overlooking your current query.
internal static var message: String { Localization.tr("Localizable", "secure_messaging.chat.leave_current_conversation.message", fallback: "You have an ongoing conversation. Starting a new conversation before ongoing ones are resolved may lead to our agents overlooking your current query.") }
/// Leave Current Conversation?
internal static var title: String { Localization.tr("Localizable", "secure_messaging.chat.leave_current_conversation.title", fallback: "Leave Current Conversation?") }
internal enum Button {
/// Leave
internal static var negative: String { Localization.tr("Localizable", "secure_messaging.chat.leave_current_conversation.button.negative", fallback: "Leave") }
/// Stay
internal static var positive: String { Localization.tr("Localizable", "secure_messaging.chat.leave_current_conversation.button.positive", fallback: "Stay") }
}
}
internal enum Unavailable {
/// Sending messages is currently not available.
internal static var message: String { Localization.tr("Localizable", "secure_messaging.chat.unavailable.message", fallback: "Sending messages is currently not available.") }
}
}
}
internal enum Survey {
internal enum Action {
/// Please provide an answer.
Expand Down
25 changes: 25 additions & 0 deletions GliaWidgets/Public/Glia/Glia+EngagementLauncher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation

extension Glia {
/// Retrieves an instance of `EngagementLauncher`.
///
/// - Parameters:
/// - queueIds: A list of queue IDs to be used for the engagement launcher. When nil, the default queues will be used.
///
/// - Returns:
/// - `EngagementLauncher` instance.
public func getEngagementLauncher(queueIds: [String]) throws -> EngagementLauncher {
let parameters = try getEngagementParameters(in: queueIds)
return try EngagementLauncher { [weak self] engagementKind, sceneProvider in
try self?.resolveEngangementState(
engagementKind: engagementKind,
sceneProvider: sceneProvider,
configuration: parameters.configuration,
interactor: parameters.interactor,
features: parameters.features,
viewFactory: parameters.viewFactory,
ongoingEngagementMediaStreams: parameters.ongoingEngagementMediaStreams
)
}
}
}
22 changes: 22 additions & 0 deletions GliaWidgets/Public/Glia/Glia+EntryWidget.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation

extension Glia {
/// Retrieves an instance of `EntryWidget`.
///
/// - Parameters:
/// - queueIds: A list of queue IDs to be used for the engagement launcher. When nil, the default queues will be used.
///
/// - Returns:
/// - `EntryWidget` instance.
public func getEntryWidget(queueIds: [String]) throws -> EntryWidget {
EntryWidget(
queueIds: queueIds,
environment: .init(
queuesMonitor: environment.queuesMonitor,
engagementLauncher: try getEngagementLauncher(queueIds: queueIds),
theme: theme,
isAuthenticated: environment.isAuthenticated
)
)
}
}
99 changes: 71 additions & 28 deletions GliaWidgets/Public/Glia/Glia+StartEngagement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,40 +24,40 @@ extension Glia {
in queueIds: [String] = [],
sceneProvider: SceneProvider? = nil
) throws {
let parameters = try getEngagementParameters(in: queueIds)

try resolveEngangementState(
engagementKind: engagementKind,
sceneProvider: sceneProvider,
configuration: parameters.configuration,
interactor: parameters.interactor,
features: parameters.features,
viewFactory: parameters.viewFactory,
ongoingEngagementMediaStreams: parameters.ongoingEngagementMediaStreams
)
}

/// Set up and returns parameters needed to start or restore engagement
func getEngagementParameters(in queueIds: [String] = []) throws -> EngagementParameters {
// In order to align behaviour between platforms,
// `GliaError.engagementExists` is no longer thrown,
// instead engagement is getting restored.
guard let configuration = self.configuration else { throw GliaError.sdkIsNotConfigured }
guard let configuration else {
throw GliaError.sdkIsNotConfigured
}

guard let interactor else {
loggerPhase.logger.prefixed(Self.self).warning("Interactor is missing")
throw GliaError.sdkIsNotConfigured
}

// Interactor is initialized during configuration, which means that queueIds need
// to be set in interactor when startEngagement is called.
self.interactor?.setQueuesIds(queueIds)
interactor.setQueuesIds(queueIds)

// It is assumed that `features` to be provided from `configure` or via deprecated `startEngagement` method.
let features = self.features ?? []

if let engagement = environment.coreSdk.getCurrentEngagement() {
if engagement.source == .callVisualizer {
throw GliaError.callVisualizerEngagementExists
} else {
guard let interactor else {
loggerPhase.logger.prefixed(Self.self).warning("Interactor is missing")
return
}
if let rootCoordinator {
rootCoordinator.maximize()
} else {
self.restoreOngoingEngagement(
configuration: configuration,
currentEngagement: engagement,
interactor: interactor,
features: features,
maximize: true
)
}
return
}
}

// Apply company name to theme and get the modified theme
let modifiedTheme = applyCompanyName(using: configuration, theme: theme)

Expand All @@ -78,10 +78,44 @@ extension Glia {
ongoingEngagementMediaStreams = .init(audio: media.audio, video: nil)
}

guard let interactor else {
loggerPhase.logger.prefixed(Self.self).warning("Interactor is missing")
return
return EngagementParameters(
viewFactory: viewFactory,
interactor: interactor,
ongoingEngagementMediaStreams: ongoingEngagementMediaStreams,
features: features,
configuration: configuration
)
}

func resolveEngangementState(
engagementKind: EngagementKind,
sceneProvider: SceneProvider?,
configuration: Configuration,
interactor: Interactor,
features: Features,
viewFactory: ViewFactory,
ongoingEngagementMediaStreams: Engagement.Media?
) throws {
if let engagement = environment.coreSdk.getCurrentEngagement() {
if engagement.source == .callVisualizer {
throw GliaError.callVisualizerEngagementExists
} else {
if let rootCoordinator {
rootCoordinator.maximize()
} else {
self.restoreOngoingEngagement(
configuration: configuration,
currentEngagement: engagement,
interactor: interactor,
features: features,
maximize: true
)
}
loggerPhase.logger.prefixed(Self.self).info("Engagement was restored")
return
}
}

startRootCoordinator(
with: interactor,
viewFactory: viewFactory,
Expand Down Expand Up @@ -175,4 +209,13 @@ extension Glia {
onEvent?(.maximized)
}
}

/// The `EngagementParameters` encapsulates all parameters required to initiate or restore the coordinator
struct EngagementParameters {
let viewFactory: ViewFactory
let interactor: Interactor
let ongoingEngagementMediaStreams: Engagement.Media?
let features: Features
let configuration: Configuration
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "send-message-unavailable-info.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
Binary file not shown.
Loading
Loading