Created
September 1, 2023 09:37
-
-
Save saroar/d08584e16773890fb70e0ee2f45bc62c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import BSON | |
import SwiftUI | |
import APIClient | |
import LoggerKit | |
import Foundation | |
import ImagePicker | |
import ECSharedModels | |
import VNRecognizeFeature | |
import AttachmentS3Client | |
import UserDefaultsClient | |
import ComposableStoreKit | |
import LocalDatabaseClient | |
import FoundationExtension | |
import ComposableArchitecture | |
import SettingsFeature | |
extension String: Identifiable { | |
public typealias ID = Int | |
public var id: Int { | |
return hash | |
} | |
} | |
public struct GenericPassForm: ReducerProtocol { | |
public enum ImageFor: String, Equatable { | |
case logo, avatar, card | |
} | |
public struct State: Equatable { | |
public init( | |
isActivityIndicatorVisible: Bool = false, | |
storeKitState: StoreKitReducer.State = .init(), | |
isUploadingImage: Bool = false, | |
logoImage: UIImage? = nil, | |
avartarImage: UIImage? = nil, | |
cardImage: UIImage? = nil, | |
imageFor: ImageFor = .avatar, | |
imageURLS: [ImageURL] = [], | |
isFormValid: Bool = false, | |
isAuthorized: Bool = true, | |
user: UserOutput? = nil, | |
vCard: VCard, | |
telephone: VCard.Telephone = .empty, | |
email: String = "" | |
) { | |
self.isActivityIndicatorVisible = isActivityIndicatorVisible | |
self.storeKitState = storeKitState | |
self.isUploadingImage = isUploadingImage | |
self.logoImage = logoImage | |
self.avartarImage = avartarImage | |
self.cardImage = cardImage | |
self.imageFor = imageFor | |
self.isFormValid = isFormValid | |
self.isAuthorized = isAuthorized | |
self.user = user | |
self.vCard = vCard | |
self.telephone = telephone | |
self.email = email | |
} | |
@BindingState public var vCard: VCard | |
@BindingState public var telephone: VCard.Telephone | |
@BindingState public var email: String | |
@BindingState public var storeKitState: StoreKitReducer.State | |
@PresentationState public var imagePicker: ImagePickerReducer.State? | |
@PresentationState public var digitalCardDesign: CardDesignListReducer.State? | |
public var pass: Pass = .draff | |
public var colorPalette: ColorPalette = .default | |
public var isActivityIndicatorVisible = false | |
public var isUploadingImage: Bool = false | |
public var logoImage: UIImage? | |
public var avartarImage: UIImage? | |
public var cardImage: UIImage? | |
public var imageFor: ImageFor = .avatar | |
public var isFormValid: Bool = false | |
public var isAuthorized: Bool = true | |
public var user: UserOutput? = nil | |
public var walletPass: WalletPass? = nil | |
public var bottomID: Int = 9 | |
public var isCustomProduct: Bool = false | |
} | |
public enum Action: BindableAction, Equatable { | |
case binding(BindingAction<State>) | |
case imagePicker(PresentationAction<ImagePickerReducer.Action>) | |
case digitalCardDesign(PresentationAction<CardDesignListReducer.Action>) | |
case dcdSheetIsPresentedButtonTapped | |
case onAppear | |
case isUploadingImage | |
case isImagePicker(isPresented: Bool) | |
case uploadAvatar(_ image: UIImage) | |
case createAttachment(_ attachment: AttachmentInOutPut) | |
case imageUploadResponse(TaskResult<String>) | |
case attacmentResponse(TaskResult<AttachmentInOutPut>) | |
case imageFor(ImageFor) | |
case recognizeText(VNRecognizeResponse) | |
case createPass | |
case buildPKPassFrom(url: String) | |
case passResponse(TaskResult<WalletPassResponse>) | |
case openSheetLogin(Bool) | |
case storeKit(StoreKitReducer.Action) | |
case buyProduct | |
case saveToServer | |
case dismissView | |
case update(isAuthorized: Bool) | |
case addOneMoreEmailSection | |
case removeEmailSection(by: UUID) | |
case addOneMoreTelephoneSection | |
case removeTelephoneSection(by: UUID) | |
case addOneMoreAddressSection | |
case removeAddressSection(by: UUID) | |
} | |
public init() {} | |
@Dependency(\.build) var build | |
@Dependency(\.apiClient) var apiClient | |
@Dependency(\.userDefaults) var userDefaults | |
@Dependency(\.keychainClient) var keychainClient | |
@Dependency(\.vnRecognizeClient) var vnRecognizeClient | |
@Dependency(\.attachmentS3Client) var attachmentS3Client | |
@Dependency(\.localDatabase) var localDatabase | |
@Dependency(\.dismiss) var dismass | |
public var body: some ReducerProtocol<State, Action> { | |
BindingReducer() | |
Scope(state: \.storeKitState, action: /Action.storeKit) { | |
StoreKitReducer() | |
} | |
Reduce(self.core) | |
.ifLet(\.$imagePicker, action: /Action.imagePicker) { | |
ImagePickerReducer() | |
} | |
.ifLet(\.$digitalCardDesign, action: /Action.digitalCardDesign) { | |
CardDesignListReducer() | |
} | |
} | |
func core(state: inout State, action: Action) -> EffectTask<Action> { | |
switch action { | |
case .binding(\.$vCard): | |
let emailValidationCheck = state.vCard.emails.count == state.vCard.emails.filter({ $0.text.isEmailValid == true }).count | |
let imageMoreThenThree = state.vCard.imageURLs.count >= 3 | |
state.isFormValid = state.vCard.isVCardValid && emailValidationCheck && imageMoreThenThree | |
sharedLogger.logError("isVCardValid: \(state.vCard.isVCardValid) emailValidationCheck:\(emailValidationCheck) imageMoreThenThree:\(imageMoreThenThree)") | |
return .none | |
case .binding: | |
return .none | |
// MARK: - .onAppear | |
case .onAppear: | |
state.isAuthorized = userDefaults.boolForKey(UserDefaultKey.isAuthorized.rawValue) | |
do { | |
state.user = try self.keychainClient.readCodable(.user, self.build.identifier(), UserOutput.self) | |
} catch { } | |
if TARGET_OS_SIMULATOR == 1 { | |
state.vCard.imageURLs = ImageURL.draff | |
} | |
return .run { send in | |
await send(.storeKit(.fetchProduct)) | |
} | |
case .openSheetLogin: | |
return .none | |
case .isUploadingImage: | |
return .none | |
case .isImagePicker(isPresented: let isPresented): | |
state.imagePicker = isPresented | |
? ImagePickerReducer.State(showingImagePicker: true, selectType: .single) | |
: nil | |
return .none | |
case .uploadAvatar: | |
state.isUploadingImage = true | |
return .none | |
// MARK: - .createAttachment | |
case .createAttachment(let attachment): | |
guard let id = state.user?.id else { | |
return .none | |
} | |
return .task { [imageFor = state.imageFor] in | |
.attacmentResponse( | |
await TaskResult { | |
if TARGET_OS_SIMULATOR == 1 { | |
if imageFor == .avatar { | |
return AttachmentInOutPut.thumbnail | |
} | |
if imageFor == .logo { | |
return AttachmentInOutPut.logo | |
} | |
} | |
return try await apiClient.request( | |
for: .authEngine(.users(.user(id: id, route: .attachments(.create(input: attachment))))), | |
as: AttachmentInOutPut.self, | |
decoder: .iso8601 | |
) | |
} | |
) | |
} | |
case .imageUploadResponse(.success(let imageURL)): | |
switch state.imageFor { | |
case .logo: | |
state.vCard.imageURLs.append(.init(type: .logo, urlString: imageURL)) | |
state.vCard.imageURLs.append(.init(type: .icon, urlString: imageURL)) | |
case .avatar: | |
sharedLogger.log(imageURL) | |
state.vCard.imageURLs.append(.init(type: .thumbnail, urlString: imageURL)) | |
case .card: | |
sharedLogger.log(imageURL) | |
} | |
sharedLogger.log(imageURL) | |
return .none | |
case .imageUploadResponse(.failure(let error)): | |
sharedLogger.logError(error) | |
return .none | |
case let .imagePicker(.presented(.picked(result: image))): | |
state.isUploadingImage = true | |
state.imagePicker = nil | |
switch state.imageFor { | |
case .logo: | |
state.logoImage = image | |
guard let currentUserID = state.user?.id else { | |
return .none | |
} | |
return .task { [serialNumber = state.pass.serialNumber] in | |
await .imageUploadResponse( | |
TaskResult { | |
if TARGET_OS_SIMULATOR == 1 { | |
return "https://learnplaygrow.ams3.digitaloceanspaces.com/uploads/images/9155F894-E500-453A-A691-6CDE8F722BDF/CECF3925-180E-4373-A15E-E7876760D18F/logo.png" | |
} | |
return try await attachmentS3Client.uploadImageToS3( | |
image, .init( | |
passId: serialNumber, | |
compressionQuality: .lowest, | |
type: .png, | |
passImagesType: .logo, | |
userId: currentUserID.hexString | |
) | |
) | |
} | |
) | |
} | |
case .avatar: | |
state.avartarImage = image | |
guard let currentUserID = state.user?.id else { | |
return .none | |
} | |
return .task { [serialNumber = state.pass.serialNumber] in | |
await .imageUploadResponse( | |
TaskResult { | |
if TARGET_OS_SIMULATOR == 1 { | |
return "https://learnplaygrow.ams3.digitaloceanspaces.com/uploads/images/DC6E2827-FF38-4038-A3BB-6F2C40695EC5/CECF3925-180E-4373-A15E-E7876760D18F/thumbnail.png" | |
} | |
return try await attachmentS3Client.uploadImageToS3( | |
image, .init( | |
passId: serialNumber, | |
compressionQuality: .lowest, | |
type: .png, | |
passImagesType: .thumbnail, | |
userId: currentUserID.hexString | |
) | |
) | |
} | |
) | |
} | |
case .card: | |
state.cardImage = image | |
return .run { [imageFor = state.imageFor, cardImage = state.cardImage] send in | |
if imageFor == .card { | |
let vnRecognizeResponse = try await vnRecognizeClient.recognizeTextRequest(cardImage!) | |
await send(.recognizeText(vnRecognizeResponse)) | |
} | |
} | |
} | |
// MARK: - .imagePicker | |
case .imagePicker: | |
return .none | |
case .attacmentResponse(.success(let attachmentResponse)): | |
state.isUploadingImage = false | |
switch state.imageFor { | |
case .logo: | |
if let image = attachmentResponse.imageUrlString { | |
// state.imageURLs.insert(image, at: 0) | |
} | |
return .none | |
case .avatar: | |
if let image = attachmentResponse.imageUrlString { | |
// state.imageURLs.insert(image, at: 0) | |
} | |
return .none | |
case .card: | |
return .none | |
} | |
case .attacmentResponse(.failure(let error)): | |
sharedLogger.logError(error) | |
return .none | |
case .imageFor(let type): | |
state.imageFor = type | |
return .none | |
case .recognizeText(let response): | |
switch response.textType { | |
case .plain: | |
let vCard = textToVcard(from: response) | |
state.vCard = vCard | |
case .vcard: | |
let vCard = textToVcard(from: response) | |
state.vCard = vCard | |
} | |
return .none | |
// MARK: - CreatePass | |
case .createPass: | |
sharedLogger.log("create pass tapped") | |
if !state.isAuthorized { | |
return .run { send in | |
await send(.openSheetLogin(true)) | |
} | |
} | |
do { | |
state.user = try self.keychainClient.readCodable(.user, self.build.identifier(), UserOutput.self) | |
} catch { | |
//state.alert = .init(title: TextState("Missing you id! please login again!")) | |
sharedLogger.logError("Missing Current user!") | |
return .none | |
} | |
guard let currentUserID = state.user?.id else { | |
sharedLogger.logError("Missing you id! please login again!") | |
return .none | |
} | |
if !state.isFormValid { | |
sharedLogger.log("FormValid is not valid") | |
return .none | |
} | |
let walletPass = WalletPass( | |
_id: .init(), | |
ownerId: currentUserID, | |
vCard: state.vCard, | |
colorPalette: .default | |
) | |
state.walletPass = walletPass | |
sharedLogger.log("send request for buy product after create pass tapped") | |
return .run { send in | |
await send(.buyProduct) | |
} | |
case .buyProduct: | |
let product: StoreKitClient.Product | |
switch state.storeKitState.type { | |
case .basic: | |
guard let basicProduct = state.storeKitState.products.first else { | |
return .none | |
} | |
product = basicProduct | |
case .custom: | |
guard let customProduct = state.storeKitState.products.last else { | |
return .none | |
} | |
product = customProduct | |
} | |
return .run { send in | |
await send(.storeKit(.tappedProduct(product))) | |
} | |
case .storeKit(.buySuccess): | |
state.walletPass?.isPaid = true | |
guard let wp = state.walletPass | |
else { | |
return .none | |
} | |
/// draff cvard make it empty again | |
return .run { send in | |
do { | |
try await localDatabase.create(wp: wp) | |
await send(.saveToServer) | |
} catch { | |
sharedLogger.logError("create localdatabase error:- \(error.localizedDescription)") | |
} | |
} | |
case .saveToServer: | |
/// draff cvard make it empty again | |
guard let wp = state.walletPass else { | |
return .none | |
} | |
state.isActivityIndicatorVisible = true | |
return .task { | |
.passResponse( | |
await TaskResult { | |
try await apiClient.request( | |
for: .walletPasses(.create(input: wp)), | |
as: WalletPassResponse.self | |
) | |
} | |
) | |
} | |
case .passResponse(.success(let response)): | |
state.isActivityIndicatorVisible = false | |
guard | |
let wp = state.walletPass | |
else { | |
return .none | |
} | |
return .run { send in | |
do { | |
try await localDatabase.update(wp: wp) | |
} catch { | |
sharedLogger.logError("create localdatabase error:- \(error)") | |
} | |
await send(.buildPKPassFrom(url: response.urlString)) | |
await self.dismass() | |
} | |
case .passResponse(.failure(let error)): | |
state.isActivityIndicatorVisible = false | |
sharedLogger.logError("passResponse error:- \(error)") | |
return .none | |
case .buildPKPassFrom: | |
return .none | |
case .storeKit: | |
return .none | |
case .dismissView: | |
return .none | |
case .update(isAuthorized: let bool): | |
state.isAuthorized = bool | |
return .none | |
case .addOneMoreEmailSection: | |
let email = VCard.Email(text: "") | |
state.vCard.emails.append(email) | |
return .none | |
case .removeEmailSection(by: let uuid): | |
state.vCard.emails.removeAll(where: { $0.id == uuid }) | |
return .none | |
case .addOneMoreTelephoneSection: | |
let telephone = VCard.Telephone.init(type: .cell, number: "") | |
state.vCard.telephones.append(telephone) | |
return .none | |
case .removeTelephoneSection(by: let uuid): | |
state.vCard.telephones.removeAll(where: { $0.id == uuid }) | |
return .none | |
case .addOneMoreAddressSection: | |
state.vCard.addresses.append(.init(type: .parcel, postOfficeAddress: "", extendedAddress: nil, street: "", locality: "", region: nil, postalCode: "", country: "")) | |
return .none | |
case .removeAddressSection(by: let uuid): | |
state.vCard.addresses.removeAll(where: { $0.id == uuid }) | |
return .none | |
// MARK: - DigitalCardDesign | |
case .digitalCardDesign(.dismiss): | |
if let colorPalette = state.digitalCardDesign?.selectedColorPalattle { | |
state.colorPalette = colorPalette | |
} | |
return .none | |
case .digitalCardDesign: | |
return .none | |
case .dcdSheetIsPresentedButtonTapped: | |
state.digitalCardDesign = .init(vCard: state.vCard) | |
return .none | |
} | |
} | |
func textToVcard(from vnRecognizeResponse: VNRecognizeResponse) -> VCard { | |
var vCard = VCard( | |
contact: VCard.Contact.empty, | |
formattedName: "", | |
organization: nil, | |
position: "", | |
website: "", | |
socialMedia: .empty | |
) | |
var vCardAddress = VCard.Address( | |
type: .work, | |
postOfficeAddress: "nil", | |
extendedAddress: nil, | |
street: "", | |
locality: "", | |
region: nil, | |
postalCode: "", | |
country: "" | |
) | |
switch vnRecognizeResponse.textType { | |
case .plain: | |
if let stringValue = vnRecognizeResponse.string { | |
let detector = NSDataDetector(types: .all) | |
detector.enumerateMatches(in: stringValue) { result, matchingFlags, bool in | |
switch result?.type { | |
case let .url(url): | |
if !vCard.urls.contains(url) { | |
vCard.urls.append(url) | |
vCard.website = url.absoluteString | |
} | |
vCard.website = url.absoluteString | |
case let .email(email: emails, url: url): | |
let components = emails.split(separator: ":", maxSplits: 1) | |
if components.count == 1 { | |
let propertyValue = String(components[0]).trimmingCharacters(in: .whitespacesAndNewlines) | |
vCard.emails.append(.init(text: propertyValue)) | |
} else { | |
_ = emails.components(separatedBy: " ").map { | |
vCard.emails.append(.init(text: $0)) | |
} | |
} | |
case let .phoneNumber(number): | |
let cleanedNumber = number.replacingOccurrences(of: " ", with: "") | |
.replacingOccurrences(of: "(", with: "") | |
.replacingOccurrences(of: ")", with: "") | |
vCard.telephones.append(.init(type: .work, number: cleanedNumber)) | |
case let .address(components: addressComponents): | |
if let street = addressComponents[.street] { | |
vCardAddress.street = street | |
} | |
if let postalCode = addressComponents[.zip] { | |
vCardAddress.postalCode = postalCode | |
} | |
if let state = addressComponents[.state] { | |
vCardAddress.region = state | |
} | |
if let city = addressComponents[.city] { | |
vCardAddress.locality = city | |
} | |
if let country = addressComponents[.country] { | |
vCardAddress.country = country | |
} | |
vCard.addresses.append(vCardAddress) | |
case let .date(date): | |
print(date) | |
case .none: | |
sharedLogger.log("NONE") | |
} | |
} | |
} | |
case .vcard: | |
if let stringValue = vnRecognizeResponse.string, | |
let vCardRes = VCard.create(from: stringValue) | |
{ | |
vCard = vCardRes | |
} | |
} | |
return vCard | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment