Skip to content
This repository was archived by the owner on Apr 30, 2024. It is now read-only.

Commit

Permalink
[iOS SDK] In-person appointment - ID based details screen (#24386)
Browse files Browse the repository at this point in the history
  • Loading branch information
tgyhlsb authored Jan 20, 2023
1 parent d3989c8 commit 3880224
Show file tree
Hide file tree
Showing 21 changed files with 445 additions and 147 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import NablaCore

protocol AppointmentRemoteDataSource {
func watchAppointments(state: RemoteAppointment.State.Enum) -> AnyPublisher<PaginatedList<RemoteAppointment>, GQLError>
func watchAppointment(withId id: UUID) -> AnyPublisher<RemoteAppointment, GQLError>
func subscribeToAppointmentsEvents() -> AnyPublisher<RemoteAppointmentsEvent, Never>
/// - Throws: ``GQLError``
func scheduleAppointment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ final class AppointmentRemoteDataSourceImpl: AppointmentRemoteDataSource {
}
}

func watchAppointment(withId id: UUID) -> AnyPublisher<RemoteAppointment, GQLError> {
gqlClient.watch(
query: GQL.GetAppointmentQuery(id: id),
policy: .returnCacheDataAndFetch
)
.map(\.appointment.appointment.fragments.appointmentFragment)
.eraseToAnyPublisher()
}

func subscribeToAppointmentsEvents() -> AnyPublisher<RemoteAppointmentsEvent, Never> {
gqlClient.subscribe(subscription: GQL.AppointmentsEventsSubscription())
.compactMap(\.appointments?.event)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// @generated
// This file was automatically generated and should not be edited.

import Apollo
import Foundation

/// GQL namespace
extension GQL {
final class GetAppointmentQuery: GraphQLQuery {
/// The raw GraphQL definition of this operation.
let operationDefinition: String =
"""
query GetAppointment($id: UUID!) {
appointment(id: $id) {
__typename
appointment {
__typename
...AppointmentFragment
}
}
}
"""

let operationName: String = "GetAppointment"

var queryDocument: String {
var document: String = operationDefinition
document.append("\n" + AppointmentFragment.fragmentDefinition)
document.append("\n" + ProviderFragment.fragmentDefinition)
document.append("\n" + UpcomingAppointmentFragment.fragmentDefinition)
document.append("\n" + FinalizedAppointmentFragment.fragmentDefinition)
document.append("\n" + LocationFragment.fragmentDefinition)
document.append("\n" + AddressFragment.fragmentDefinition)
document.append("\n" + LivekitRoomFragment.fragmentDefinition)
document.append("\n" + LivekitRoomOpenStatusFragment.fragmentDefinition)
document.append("\n" + LivekitRoomClosedStatusFragment.fragmentDefinition)
return document
}

var id: GQL.UUID

init(id: GQL.UUID) {
self.id = id
}

var variables: GraphQLMap? {
return ["id": id]
}

struct Data: GraphQLSelectionSet {
static let possibleTypes: [String] = ["Query"]

static var selections: [GraphQLSelection] {
return [
GraphQLField("appointment", arguments: ["id": GraphQLVariable("id")], type: .nonNull(.object(Appointment.selections))),
]
}

private(set) var resultMap: ResultMap

init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}

init(appointment: Appointment) {
self.init(unsafeResultMap: ["__typename": "Query", "appointment": appointment.resultMap])
}

var appointment: Appointment {
get {
return Appointment(unsafeResultMap: resultMap["appointment"]! as! ResultMap)
}
set {
resultMap.updateValue(newValue.resultMap, forKey: "appointment")
}
}

struct Appointment: GraphQLSelectionSet {
static let possibleTypes: [String] = ["AppointmentOutput"]

static var selections: [GraphQLSelection] {
return [
GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
GraphQLField("appointment", type: .nonNull(.object(Appointment.selections))),
]
}

private(set) var resultMap: ResultMap

init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}

init(appointment: Appointment) {
self.init(unsafeResultMap: ["__typename": "AppointmentOutput", "appointment": appointment.resultMap])
}

var __typename: String {
get {
return resultMap["__typename"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "__typename")
}
}

var appointment: Appointment {
get {
return Appointment(unsafeResultMap: resultMap["appointment"]! as! ResultMap)
}
set {
resultMap.updateValue(newValue.resultMap, forKey: "appointment")
}
}

struct Appointment: GraphQLSelectionSet {
static let possibleTypes: [String] = ["Appointment"]

static var selections: [GraphQLSelection] {
return [
GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
GraphQLFragmentSpread(AppointmentFragment.self),
]
}

private(set) var resultMap: ResultMap

init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}

var __typename: String {
get {
return resultMap["__typename"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "__typename")
}
}

var fragments: Fragments {
get {
return Fragments(unsafeResultMap: resultMap)
}
set {
resultMap += newValue.resultMap
}
}

struct Fragments {
private(set) var resultMap: ResultMap

init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}

var appointmentFragment: AppointmentFragment {
get {
return AppointmentFragment(unsafeResultMap: resultMap)
}
set {
resultMap += newValue.resultMap
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
query GetAppointment($id: UUID!) {
appointment(id: $id) {
appointment {
...AppointmentFragment
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import NablaCore

protocol AppointmentRepository {
func watchAppointments(state: Appointment.State) -> AnyPublisher<PaginatedList<Appointment>, NablaError>
func watchAppointment(withId id: UUID) -> AnyPublisher<Appointment, NablaError>
/// - Throws: ``NablaError``
func scheduleAppointment(
location: LocationType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ final class AppointmentRepositoryImpl: AppointmentRepository {
.eraseToAnyPublisher()
}

func watchAppointment(withId id: UUID) -> AnyPublisher<Appointment, NablaError> {
let transformer = RemoteAppointmentTransformer(logger: logger)
return remoteDataSource.watchAppointment(withId: id)
.map { transformer.transform($0) }
.mapError(GQLErrorTransformer.transform(gqlError:))
.eraseToAnyPublisher()
}

/// - Throws: ``NablaError``
func scheduleAppointment(location: LocationType, categoryId: UUID, providerId: UUID, date: Date) async throws -> Appointment {
do {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Combine
import Foundation
import NablaCore

protocol WatchAppointmentInteractor {
func execute(id: UUID) -> AnyPublisher<Appointment, NablaError>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Combine
import Foundation
import NablaCore

final class WatchAppointmentInteractorImpl: WatchAppointmentInteractor {
// MARK: - Internal

func execute(id: UUID) -> AnyPublisher<Appointment, NablaError> {
repository.watchAppointment(withId: id)
}

// MARK: Init

init(
repository: AppointmentRepository
) {
self.repository = repository
}

// MARK: - Private

private let repository: AppointmentRepository
}
4 changes: 4 additions & 0 deletions Sources/NablaScheduling/Generated/Strings+Generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ internal enum L10n {
}
/// Appointment
internal static let appointmentDetailsScreenTitle = L10n.tr("Localizable", "appointment_details_screen_title")
/// Failed to load the appointment. Please try again.
internal static let appointmentDetailsScreenWatchAppointmentErrorMessage = L10n.tr("Localizable", "appointment_details_screen_watch_appointment_error_message")
/// Something went wrong
internal static let appointmentDetailsScreenWatchAppointmentErrorTitle = L10n.tr("Localizable", "appointment_details_screen_watch_appointment_error_title")
/// Schedule an appointment
internal static let appointmentsScreenActionButtonLabel = L10n.tr("Localizable", "appointments_screen_action_button_label")
/// Join the call
Expand Down
4 changes: 4 additions & 0 deletions Sources/NablaScheduling/NablaSchedulingClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ public final class NablaSchedulingClient: SchedulingClient {
container.watchAppointmentsInteractor.execute(state: state)
}

func watchAppointment(id: UUID) -> AnyPublisher<Appointment, NablaError> {
container.watchAppointmentInteractor.execute(id: id)
}

func watchCategories() -> AnyPublisher<[Category], NablaError> {
container.watchCategoriesInteractor.execute()
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/NablaScheduling/NablaSchedulingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ final class NablaSchedulingContainer {
let addressFormatter: AddressFormatter
let universalLinkGenerator: CompositeUniversalLinkGenerator
let watchAppointmentsInteractor: WatchAppointmentsInteractor
let watchAppointmentInteractor: WatchAppointmentInteractor
let watchCategoriesInteractor: WatchCategoriesInteractor
let watchAvailabilitySlotsInteractor: WatchAvailabilitySlotsInteractor
let scheduleAppointmentInteractor: ScheduleAppointmentInteractor
Expand Down Expand Up @@ -68,6 +69,7 @@ final class NablaSchedulingContainer {
availabilitySlotRepository = AvailabilitySlotRepositoryImpl(remoteDataSource: availabilitySlotRemoteDataSource)

watchAppointmentsInteractor = WatchAppointmentsInteractorImpl(repository: appointmentRepository)
watchAppointmentInteractor = WatchAppointmentInteractorImpl(repository: appointmentRepository)
watchCategoriesInteractor = WatchCategoriesInteractorImpl(repository: availabilitySlotRepository)
watchAvailabilitySlotsInteractor = WatchAvailabilitySlotsInteractorImpl(repository: availabilitySlotRepository)
scheduleAppointmentInteractor = ScheduleAppointmentInteractorImpl(repository: appointmentRepository)
Expand Down
14 changes: 14 additions & 0 deletions Sources/NablaScheduling/NablaSchedulingViewFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import UIKit
public protocol NablaSchedulingViewFactory: SchedulingViewFactory {
func createAppointmentListViewController(delegate: AppointmentListDelegate) -> UIViewController
func presentScheduleAppointmentNavigationController(from presentingViewController: UIViewController)
func createAppointmentDetailsViewController(appointmentId: UUID, delegate: AppointmentDetailsDelegate) -> UIViewController
func createAppointmentDetailsViewController(appointment: Appointment, delegate: AppointmentDetailsDelegate) -> UIViewController
}

Expand Down Expand Up @@ -53,6 +54,19 @@ public class NablaSchedulingViewFactoryImpl: NablaSchedulingViewFactory, Interna
presentingViewController.present(viewController, animated: true)
}

public func createAppointmentDetailsViewController(appointmentId: UUID, delegate: AppointmentDetailsDelegate) -> UIViewController {
let viewModel = AppointmentDetailsViewModelImpl(
appointmentId: appointmentId,
delegate: delegate,
client: client,
addressFormatter: client.container.addressFormatter,
universalLinkGenerator: client.container.universalLinkGenerator
)
let viewController = AppointmentDetailsViewController(viewModel: viewModel)
viewController.navigationItem.largeTitleDisplayMode = .never
return viewController
}

public func createAppointmentDetailsViewController(appointment: Appointment, delegate: AppointmentDetailsDelegate) -> UIViewController {
let viewModel = AppointmentDetailsViewModelImpl(
appointment: appointment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"appointment_details_screen_cancel_appointment_modal_confirm_button" = "Confirm cancellation";
"appointment_details_screen_cancel_appointment_modal_close_button" = "Keep appointment";

"appointment_details_screen_watch_appointment_error_title" = "Something went wrong";
"appointment_details_screen_watch_appointment_error_message" = "Failed to load the appointment. Please try again.";

"appointment_details_screen_cancel_appointment_error_title" = "Something went wrong";
"appointment_details_screen_cancel_appointment_error_message" = "Failed to cancel the appointment. Please try again.";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"appointment_details_screen_cancel_appointment_modal_confirm_button" = "Confirmer l’annulation";
"appointment_details_screen_cancel_appointment_modal_close_button" = "Conserver le rendez-vous";

"appointment_details_screen_watch_appointment_error_title" = "Une erreur est survenue";
"appointment_details_screen_watch_appointment_error_message" = "Impossible de trouver le rendez-vous. Réessayez.";

"appointment_details_screen_cancel_appointment_error_title" = "Une erreur est survenue";
"appointment_details_screen_cancel_appointment_error_message" = "Impossible d'annuler le rendez-vous. Réessayez.";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ final class AppointmentDetailsViewController: UIViewController {

// MARK: Subviews

private lazy var loadingView: UIActivityIndicatorView = {
let view = UIActivityIndicatorView()
view.color = NablaTheme.Shared.loadingViewIndicatorTintColor
view.startAnimating()
return view
}()

private lazy var headerView: AppointmentDetailsView = {
let view = AppointmentDetailsView(
frame: .zero,
Expand Down Expand Up @@ -95,20 +102,31 @@ final class AppointmentDetailsViewController: UIViewController {

view.addSubview(bottomContainer)
bottomContainer.nabla.pin(to: view.safeAreaLayoutGuide, edges: [.leading, .bottom, .trailing])

view.addSubview(loadingView)
loadingView.nabla.constraintToCenterInSuperView()
}

// MARK: ViewModel

private func observeViewModel() {
_viewModel.onChange { [weak self] viewModel in
guard let self = self else { return }
self.updateDetails()
self.update(provider: viewModel.provider)
self.updateModal()
switch viewModel.state {
case .loading:
self.loadingView.isHidden = false
self.scrollableContainer.isHidden = true
case let .ready(viewModel):
self.loadingView.isHidden = true
self.scrollableContainer.isHidden = false
self.updateDetails(viewModel)
self.update(provider: viewModel.provider)
}
}
}

private func updateDetails() {
private func updateDetails(_ viewModel: AppointmentsDetailsViewItem) {
headerView.caption = viewModel.caption
headerView.captionIcon = viewModel.captionIcon
headerView.details1 = viewModel.details1
Expand Down
Loading

0 comments on commit 3880224

Please sign in to comment.