From 4969968c53bb7f817e782f9650606636e6528db1 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 25 May 2022 21:08:46 -0700 Subject: [PATCH 1/6] update swift gql gen. add a subscriptions view --- .../App/Views/Profile/ProfileView.swift | 4 + .../App/Views/Profile/Subscriptions.swift | 113 +++ .../Services/DataService/GQLSchema.swift | 774 ++++++++++++++++++ 3 files changed, 891 insertions(+) create mode 100644 apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index 721680f70..70aa2f66e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -78,6 +78,10 @@ struct ProfileView: View { NavigationLink(destination: NewsletterEmailsView()) { Text("Emails") } + + NavigationLink(destination: SubscriptionsView()) { + Text("Subscriptions") + } } Section { diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift new file mode 100644 index 000000000..675b91c7c --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift @@ -0,0 +1,113 @@ +import Models +import Services +import SwiftUI +import Views + +@MainActor final class SubscriptionsViewModel: ObservableObject { + @Published var isLoading = false + @Published var emails = [NewsletterEmail]() + + func loadSubscriptions(dataService _: DataService) async { + isLoading = true + +// if let subscriptions = try? await dataService.subscriptions() { +// await dataService.viewContext.perform { [weak self] in +// self?.emails = objectIDs.compactMap { dataService.viewContext.object(with: $0) as? NewsletterEmail } +// } +// } + + isLoading = false + } + + func loadEmails(dataService: DataService) async { + isLoading = true + + if let objectIDs = try? await dataService.newsletterEmails() { + await dataService.viewContext.perform { [weak self] in + self?.emails = objectIDs.compactMap { dataService.viewContext.object(with: $0) as? NewsletterEmail } + } + } + + isLoading = false + } + + func createEmail(dataService: DataService) async { + isLoading = true + + if let objectID = try? await dataService.createNewsletter() { + await dataService.viewContext.perform { [weak self] in + if let item = dataService.viewContext.object(with: objectID) as? NewsletterEmail { + self?.emails.insert(item, at: 0) + } + } + } + + isLoading = false + } +} + +struct SubscriptionsView: View { + @EnvironmentObject var dataService: DataService + @StateObject var viewModel = SubscriptionsViewModel() + let footerText = "Add PDFs to your library, or subscribe to emails using an Omnivore email address." + + var body: some View { + Group { + #if os(iOS) + Form { + innerBody + } + #elseif os(macOS) + List { + innerBody + } + .listStyle(InsetListStyle()) + #endif + } + .task { await viewModel.loadEmails(dataService: dataService) } + } + + private var innerBody: some View { + Group { + Section(footer: Text(footerText)) { + Button( + action: { + Task { await viewModel.createEmail(dataService: dataService) } + }, + label: { + HStack { + Image(systemName: "plus.circle.fill").foregroundColor(.green) + Text("Create a new email address") + Spacer() + } + } + ) + .disabled(viewModel.isLoading) + } + + if !viewModel.emails.isEmpty { + Section(header: Text("Existing Emails (Tap to copy)")) { + ForEach(viewModel.emails) { newsletterEmail in + Button( + action: { + #if os(iOS) + UIPasteboard.general.string = newsletterEmail.email + #endif + + #if os(macOS) + let pasteBoard = NSPasteboard.general + pasteBoard.clearContents() + pasteBoard.writeObjects([newsletterEmail.unwrappedEmail as NSString]) + #endif + + Snackbar.show(message: "Email copied") + }, + label: { Text(newsletterEmail.unwrappedEmail) } + ) + } + } + } + } + .navigationTitle("Emails") + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index 337fe8a6d..d2faed4b9 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -20,6 +20,136 @@ extension Objects.Subscription: GraphQLWebSocketOperation { // MARK: - Objects enum Objects {} +extension Objects { + struct AddPopularReadError { + let __typename: TypeName = .addPopularReadError + let errorCodes: [String: [Enums.AddPopularReadErrorCode]] + + enum TypeName: String, Codable { + case addPopularReadError = "AddPopularReadError" + } + } +} + +extension Objects.AddPopularReadError: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.AddPopularReadErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + errorCodes = map["errorCodes"] + } +} + +extension Fields where TypeLock == Objects.AddPopularReadError { + func errorCodes() throws -> [Enums.AddPopularReadErrorCode] { + let field = GraphQLField.leaf( + name: "errorCodes", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.errorCodes[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return [] + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias AddPopularReadError = Selection +} + +extension Objects { + struct AddPopularReadSuccess { + let __typename: TypeName = .addPopularReadSuccess + let pageId: [String: String] + + enum TypeName: String, Codable { + case addPopularReadSuccess = "AddPopularReadSuccess" + } + } +} + +extension Objects.AddPopularReadSuccess: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "pageId": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + pageId = map["pageId"] + } +} + +extension Fields where TypeLock == Objects.AddPopularReadSuccess { + func pageId() throws -> String { + let field = GraphQLField.leaf( + name: "pageId", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.pageId[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias AddPopularReadSuccess = Selection +} + extension Objects { struct ArchiveLinkError { let __typename: TypeName = .archiveLinkError @@ -6691,6 +6821,7 @@ extension Selection where TypeLock == Never, Type == Never { extension Objects { struct Mutation { let __typename: TypeName = .mutation + let addPopularRead: [String: Unions.AddPopularReadResult] let createArticle: [String: Unions.CreateArticleResult] let createArticleSavingRequest: [String: Unions.CreateArticleSavingRequestResult] let createHighlight: [String: Unions.CreateHighlightResult] @@ -6725,11 +6856,13 @@ extension Objects { let setShareHighlight: [String: Unions.SetShareHighlightResult] let setUserPersonalization: [String: Unions.SetUserPersonalizationResult] let signup: [String: Unions.SignupResult] + let subscribe: [String: Unions.SubscribeResult] let unsubscribe: [String: Unions.UnsubscribeResult] let updateHighlight: [String: Unions.UpdateHighlightResult] let updateHighlightReply: [String: Unions.UpdateHighlightReplyResult] let updateLabel: [String: Unions.UpdateLabelResult] let updateLinkShareInfo: [String: Unions.UpdateLinkShareInfoResult] + let updatePage: [String: Unions.UpdatePageResult] let updateReminder: [String: Unions.UpdateReminderResult] let updateSharedComment: [String: Unions.UpdateSharedCommentResult] let updateUser: [String: Unions.UpdateUserResult] @@ -6754,6 +6887,10 @@ extension Objects.Mutation: Decodable { let field = GraphQLField.getFieldNameFromAlias(alias) switch field { + case "addPopularRead": + if let value = try container.decode(Unions.AddPopularReadResult?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "createArticle": if let value = try container.decode(Unions.CreateArticleResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -6890,6 +7027,10 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.SignupResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "subscribe": + if let value = try container.decode(Unions.SubscribeResult?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "unsubscribe": if let value = try container.decode(Unions.UnsubscribeResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -6910,6 +7051,10 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.UpdateLinkShareInfoResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "updatePage": + if let value = try container.decode(Unions.UpdatePageResult?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "updateReminder": if let value = try container.decode(Unions.UpdateReminderResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -6940,6 +7085,7 @@ extension Objects.Mutation: Decodable { } } + addPopularRead = map["addPopularRead"] createArticle = map["createArticle"] createArticleSavingRequest = map["createArticleSavingRequest"] createHighlight = map["createHighlight"] @@ -6974,11 +7120,13 @@ extension Objects.Mutation: Decodable { setShareHighlight = map["setShareHighlight"] setUserPersonalization = map["setUserPersonalization"] signup = map["signup"] + subscribe = map["subscribe"] unsubscribe = map["unsubscribe"] updateHighlight = map["updateHighlight"] updateHighlightReply = map["updateHighlightReply"] updateLabel = map["updateLabel"] updateLinkShareInfo = map["updateLinkShareInfo"] + updatePage = map["updatePage"] updateReminder = map["updateReminder"] updateSharedComment = map["updateSharedComment"] updateUser = map["updateUser"] @@ -6988,6 +7136,25 @@ extension Objects.Mutation: Decodable { } extension Fields where TypeLock == Objects.Mutation { + func addPopularRead(name: String, selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "addPopularRead", + arguments: [Argument(name: "name", type: "String!", value: name)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.addPopularRead[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } + func createArticle(input: InputObjects.CreateArticleInput, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "createArticle", @@ -7634,6 +7801,25 @@ extension Fields where TypeLock == Objects.Mutation { } } + func subscribe(name: String, selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "subscribe", + arguments: [Argument(name: "name", type: "String!", value: name)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.subscribe[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } + func unsubscribe(name: String, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "unsubscribe", @@ -7729,6 +7915,25 @@ extension Fields where TypeLock == Objects.Mutation { } } + func updatePage(input: InputObjects.UpdatePageInput, selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "updatePage", + arguments: [Argument(name: "input", type: "UpdatePageInput!", value: input)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.updatePage[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } + func updateReminder(input: InputObjects.UpdateReminderInput, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "updateReminder", @@ -10225,6 +10430,7 @@ extension Objects { let readingProgressAnchorIndex: [String: Int] let readingProgressPercent: [String: Double] let shortId: [String: String] + let siteName: [String: String] let slug: [String: String] let state: [String: Enums.ArticleSavingRequestStatus] let subscription: [String: String] @@ -10324,6 +10530,10 @@ extension Objects.SearchItem: Decodable { if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "siteName": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "slug": if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -10384,6 +10594,7 @@ extension Objects.SearchItem: Decodable { readingProgressAnchorIndex = map["readingProgressAnchorIndex"] readingProgressPercent = map["readingProgressPercent"] shortId = map["shortId"] + siteName = map["siteName"] slug = map["slug"] state = map["state"] subscription = map["subscription"] @@ -10682,6 +10893,21 @@ extension Fields where TypeLock == Objects.SearchItem { } } + func siteName() throws -> String? { + let field = GraphQLField.leaf( + name: "siteName", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.siteName[field.alias!] + case .mocking: + return nil + } + } + func slug() throws -> String { let field = GraphQLField.leaf( name: "slug", @@ -12332,6 +12558,137 @@ extension Selection where TypeLock == Never, Type == Never { typealias SignupSuccess = Selection } +extension Objects { + struct SubscribeError { + let __typename: TypeName = .subscribeError + let errorCodes: [String: [Enums.SubscribeErrorCode]] + + enum TypeName: String, Codable { + case subscribeError = "SubscribeError" + } + } +} + +extension Objects.SubscribeError: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.SubscribeErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + errorCodes = map["errorCodes"] + } +} + +extension Fields where TypeLock == Objects.SubscribeError { + func errorCodes() throws -> [Enums.SubscribeErrorCode] { + let field = GraphQLField.leaf( + name: "errorCodes", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.errorCodes[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return [] + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias SubscribeError = Selection +} + +extension Objects { + struct SubscribeSuccess { + let __typename: TypeName = .subscribeSuccess + let subscriptions: [String: [Objects.Subscription]] + + enum TypeName: String, Codable { + case subscribeSuccess = "SubscribeSuccess" + } + } +} + +extension Objects.SubscribeSuccess: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "subscriptions": + if let value = try container.decode([Objects.Subscription]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + subscriptions = map["subscriptions"] + } +} + +extension Fields where TypeLock == Objects.SubscribeSuccess { + func subscriptions(selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "subscriptions", + arguments: [], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.subscriptions[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias SubscribeSuccess = Selection +} + extension Objects { struct Subscription { let __typename: TypeName = .subscription @@ -13386,6 +13743,137 @@ extension Selection where TypeLock == Never, Type == Never { typealias UpdateLinkShareInfoSuccess = Selection } +extension Objects { + struct UpdatePageError { + let __typename: TypeName = .updatePageError + let errorCodes: [String: [Enums.UpdatePageErrorCode]] + + enum TypeName: String, Codable { + case updatePageError = "UpdatePageError" + } + } +} + +extension Objects.UpdatePageError: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.UpdatePageErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + errorCodes = map["errorCodes"] + } +} + +extension Fields where TypeLock == Objects.UpdatePageError { + func errorCodes() throws -> [Enums.UpdatePageErrorCode] { + let field = GraphQLField.leaf( + name: "errorCodes", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.errorCodes[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return [] + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias UpdatePageError = Selection +} + +extension Objects { + struct UpdatePageSuccess { + let __typename: TypeName = .updatePageSuccess + let updatedPage: [String: Objects.Page] + + enum TypeName: String, Codable { + case updatePageSuccess = "UpdatePageSuccess" + } + } +} + +extension Objects.UpdatePageSuccess: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "updatedPage": + if let value = try container.decode(Objects.Page?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + updatedPage = map["updatedPage"] + } +} + +extension Fields where TypeLock == Objects.UpdatePageSuccess { + func updatedPage(selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "updatedPage", + arguments: [], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.updatedPage[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias UpdatePageSuccess = Selection +} + extension Objects { struct UpdateReminderError { let __typename: TypeName = .updateReminderError @@ -14891,6 +15379,80 @@ enum Interfaces {} // MARK: - Unions enum Unions {} +extension Unions { + struct AddPopularReadResult { + let __typename: TypeName + let errorCodes: [String: [Enums.AddPopularReadErrorCode]] + let pageId: [String: String] + + enum TypeName: String, Codable { + case addPopularReadError = "AddPopularReadError" + case addPopularReadSuccess = "AddPopularReadSuccess" + } + } +} + +extension Unions.AddPopularReadResult: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.AddPopularReadErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "pageId": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!) + + errorCodes = map["errorCodes"] + pageId = map["pageId"] + } +} + +extension Fields where TypeLock == Unions.AddPopularReadResult { + func on(addPopularReadError: Selection, addPopularReadSuccess: Selection) throws -> Type { + select([GraphQLField.fragment(type: "AddPopularReadError", selection: addPopularReadError.selection), GraphQLField.fragment(type: "AddPopularReadSuccess", selection: addPopularReadSuccess.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .addPopularReadError: + let data = Objects.AddPopularReadError(errorCodes: data.errorCodes) + return try addPopularReadError.decode(data: data) + case .addPopularReadSuccess: + let data = Objects.AddPopularReadSuccess(pageId: data.pageId) + return try addPopularReadSuccess.decode(data: data) + } + case .mocking: + return addPopularReadError.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias AddPopularReadResult = Selection +} + extension Unions { struct ArchiveLinkResult { let __typename: TypeName @@ -18065,6 +18627,80 @@ extension Selection where TypeLock == Never, Type == Never { typealias SignupResult = Selection } +extension Unions { + struct SubscribeResult { + let __typename: TypeName + let errorCodes: [String: [Enums.SubscribeErrorCode]] + let subscriptions: [String: [Objects.Subscription]] + + enum TypeName: String, Codable { + case subscribeError = "SubscribeError" + case subscribeSuccess = "SubscribeSuccess" + } + } +} + +extension Unions.SubscribeResult: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.SubscribeErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "subscriptions": + if let value = try container.decode([Objects.Subscription]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!) + + errorCodes = map["errorCodes"] + subscriptions = map["subscriptions"] + } +} + +extension Fields where TypeLock == Unions.SubscribeResult { + func on(subscribeError: Selection, subscribeSuccess: Selection) throws -> Type { + select([GraphQLField.fragment(type: "SubscribeError", selection: subscribeError.selection), GraphQLField.fragment(type: "SubscribeSuccess", selection: subscribeSuccess.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .subscribeError: + let data = Objects.SubscribeError(errorCodes: data.errorCodes) + return try subscribeError.decode(data: data) + case .subscribeSuccess: + let data = Objects.SubscribeSuccess(subscriptions: data.subscriptions) + return try subscribeSuccess.decode(data: data) + } + case .mocking: + return subscribeError.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias SubscribeResult = Selection +} + extension Unions { struct SubscriptionsResult { let __typename: TypeName @@ -18509,6 +19145,80 @@ extension Selection where TypeLock == Never, Type == Never { typealias UpdateLinkShareInfoResult = Selection } +extension Unions { + struct UpdatePageResult { + let __typename: TypeName + let errorCodes: [String: [Enums.UpdatePageErrorCode]] + let updatedPage: [String: Objects.Page] + + enum TypeName: String, Codable { + case updatePageError = "UpdatePageError" + case updatePageSuccess = "UpdatePageSuccess" + } + } +} + +extension Unions.UpdatePageResult: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.UpdatePageErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "updatedPage": + if let value = try container.decode(Objects.Page?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!) + + errorCodes = map["errorCodes"] + updatedPage = map["updatedPage"] + } +} + +extension Fields where TypeLock == Unions.UpdatePageResult { + func on(updatePageError: Selection, updatePageSuccess: Selection) throws -> Type { + select([GraphQLField.fragment(type: "UpdatePageError", selection: updatePageError.selection), GraphQLField.fragment(type: "UpdatePageSuccess", selection: updatePageSuccess.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .updatePageError: + let data = Objects.UpdatePageError(errorCodes: data.errorCodes) + return try updatePageError.decode(data: data) + case .updatePageSuccess: + let data = Objects.UpdatePageSuccess(updatedPage: data.updatedPage) + return try updatePageSuccess.decode(data: data) + } + case .mocking: + return updatePageError.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias UpdatePageResult = Selection +} + extension Unions { struct UpdateReminderResult { let __typename: TypeName @@ -19048,6 +19758,17 @@ extension Selection where TypeLock == Never, Type == Never { // MARK: - Enums enum Enums {} +extension Enums { + /// AddPopularReadErrorCode + enum AddPopularReadErrorCode: String, CaseIterable, Codable { + case badRequest = "BAD_REQUEST" + + case notFound = "NOT_FOUND" + + case unauthorized = "UNAUTHORIZED" + } +} + extension Enums { /// ArchiveLinkErrorCode enum ArchiveLinkErrorCode: String, CaseIterable, Codable { @@ -19551,6 +20272,8 @@ extension Enums { extension Enums { /// SortBy enum SortBy: String, CaseIterable, Codable { + case publishedAt = "PUBLISHED_AT" + case savedAt = "SAVED_AT" case score = "SCORE" @@ -19568,6 +20291,19 @@ extension Enums { } } +extension Enums { + /// SubscribeErrorCode + enum SubscribeErrorCode: String, CaseIterable, Codable { + case alreadySubscribed = "ALREADY_SUBSCRIBED" + + case badRequest = "BAD_REQUEST" + + case notFound = "NOT_FOUND" + + case unauthorized = "UNAUTHORIZED" + } +} + extension Enums { /// SubscriptionsErrorCode enum SubscriptionsErrorCode: String, CaseIterable, Codable { @@ -19649,6 +20385,21 @@ extension Enums { } } +extension Enums { + /// UpdatePageErrorCode + enum UpdatePageErrorCode: String, CaseIterable, Codable { + case badRequest = "BAD_REQUEST" + + case forbidden = "FORBIDDEN" + + case notFound = "NOT_FOUND" + + case unauthorized = "UNAUTHORIZED" + + case updateFailed = "UPDATE_FAILED" + } +} + extension Enums { /// UpdateReminderErrorCode enum UpdateReminderErrorCode: String, CaseIterable, Codable { @@ -20604,6 +21355,29 @@ extension InputObjects { } } +extension InputObjects { + struct UpdatePageInput: Encodable, Hashable { + var description: OptionalArgument = .absent() + + var pageId: String + + var title: OptionalArgument = .absent() + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + if description.hasValue { try container.encode(description, forKey: .description) } + try container.encode(pageId, forKey: .pageId) + if title.hasValue { try container.encode(title, forKey: .title) } + } + + enum CodingKeys: String, CodingKey { + case description + case pageId + case title + } + } +} + extension InputObjects { struct UpdateReminderInput: Encodable, Hashable { var archiveUntil: Bool From e1aa68858fa63ee840da382f9418a59f820c5f2b Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 25 May 2022 22:51:41 -0700 Subject: [PATCH 2/6] show subscriptions in list view --- .../App/Views/Profile/Subscriptions.swift | 94 +++++-------------- .../Sources/Models/AppEnvironment.swift | 2 +- .../Models/DataModels/Subscription.swift | 44 +++++++++ .../Queries/SubscriptionsQuery.swift | 74 +++++++++++++++ 4 files changed, 144 insertions(+), 70 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Models/DataModels/Subscription.swift create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift index 675b91c7c..fb0de0b62 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift @@ -5,41 +5,17 @@ import Views @MainActor final class SubscriptionsViewModel: ObservableObject { @Published var isLoading = false - @Published var emails = [NewsletterEmail]() + @Published var subscriptions = [Subscription]() + @Published var popularSubscriptions = [Subscription]() + @Published var hasNetworkError = false - func loadSubscriptions(dataService _: DataService) async { + func loadSubscriptions(dataService: DataService) async { isLoading = true -// if let subscriptions = try? await dataService.subscriptions() { -// await dataService.viewContext.perform { [weak self] in -// self?.emails = objectIDs.compactMap { dataService.viewContext.object(with: $0) as? NewsletterEmail } -// } -// } - - isLoading = false - } - - func loadEmails(dataService: DataService) async { - isLoading = true - - if let objectIDs = try? await dataService.newsletterEmails() { - await dataService.viewContext.perform { [weak self] in - self?.emails = objectIDs.compactMap { dataService.viewContext.object(with: $0) as? NewsletterEmail } - } - } - - isLoading = false - } - - func createEmail(dataService: DataService) async { - isLoading = true - - if let objectID = try? await dataService.createNewsletter() { - await dataService.viewContext.perform { [weak self] in - if let item = dataService.viewContext.object(with: objectID) as? NewsletterEmail { - self?.emails.insert(item, at: 0) - } - } + do { + subscriptions = try await dataService.subscriptions() + } catch { + hasNetworkError = true } isLoading = false @@ -49,7 +25,7 @@ import Views struct SubscriptionsView: View { @EnvironmentObject var dataService: DataService @StateObject var viewModel = SubscriptionsViewModel() - let footerText = "Add PDFs to your library, or subscribe to emails using an Omnivore email address." + let footerText = "Describe subscriptions here." var body: some View { Group { @@ -64,50 +40,30 @@ struct SubscriptionsView: View { .listStyle(InsetListStyle()) #endif } - .task { await viewModel.loadEmails(dataService: dataService) } + .task { await viewModel.loadSubscriptions(dataService: dataService) } } private var innerBody: some View { Group { - Section(footer: Text(footerText)) { + ForEach(viewModel.subscriptions, id: \.subscriptionID) { subscription in Button( - action: { - Task { await viewModel.createEmail(dataService: dataService) } - }, - label: { - HStack { - Image(systemName: "plus.circle.fill").foregroundColor(.green) - Text("Create a new email address") - Spacer() - } - } + action: {}, + label: { Text(subscription.name) } ) - .disabled(viewModel.isLoading) - } - - if !viewModel.emails.isEmpty { - Section(header: Text("Existing Emails (Tap to copy)")) { - ForEach(viewModel.emails) { newsletterEmail in - Button( - action: { - #if os(iOS) - UIPasteboard.general.string = newsletterEmail.email - #endif - - #if os(macOS) - let pasteBoard = NSPasteboard.general - pasteBoard.clearContents() - pasteBoard.writeObjects([newsletterEmail.unwrappedEmail as NSString]) - #endif - - Snackbar.show(message: "Email copied") - }, - label: { Text(newsletterEmail.unwrappedEmail) } - ) - } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button( + role: .destructive, + action: { +// itemToRemove = item +// confirmationShown = true + }, + label: { + Image(systemName: "trash") + } + ) } } } - .navigationTitle("Emails") + .navigationTitle("Subscriptions") } } diff --git a/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift b/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift index e49cb03f9..e6929645f 100644 --- a/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift +++ b/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift @@ -11,7 +11,7 @@ public enum AppEnvironment: String { public static let initialAppEnvironment: AppEnvironment = { #if DEBUG #if targetEnvironment(simulator) - return .demo // could also return .local here + return .prod // could also return .local here #else return .demo #endif diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/Subscription.swift b/apple/OmnivoreKit/Sources/Models/DataModels/Subscription.swift new file mode 100644 index 000000000..10d6208a9 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Models/DataModels/Subscription.swift @@ -0,0 +1,44 @@ +import Foundation + +public struct Subscription { + public let createdAt: Date? + public let description: String? + public let subscriptionID: String + public let name: String + public let newsletterEmailAddress: String + public let status: SubscriptionStatus + public let unsubscribeHttpUrl: String? + public let unsubscribeMailTo: String? + public let updatedAt: Date? + public let url: String? + + public init( + createdAt: Date?, + description: String?, + subscriptionID: String, + name: String, + newsletterEmailAddress: String, + status: SubscriptionStatus, + unsubscribeHttpUrl: String?, + unsubscribeMailTo: String?, + updatedAt: Date?, + url: String? + ) { + self.createdAt = createdAt + self.description = description + self.subscriptionID = subscriptionID + self.name = name + self.newsletterEmailAddress = newsletterEmailAddress + self.status = status + self.unsubscribeHttpUrl = unsubscribeHttpUrl + self.unsubscribeMailTo = unsubscribeMailTo + self.updatedAt = updatedAt + self.url = url + } +} + +public enum SubscriptionStatus { + case active + case deleted + case unsubscribed +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift new file mode 100644 index 000000000..2c2976126 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/SubscriptionsQuery.swift @@ -0,0 +1,74 @@ +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func subscriptions() async throws -> [Subscription] { + enum QueryResult { + case success(result: [Subscription]) + case error(error: String) + } + + let subsciptionSelection = Selection.Subscription { + Subscription( + createdAt: try $0.createdAt().value, + description: try $0.description(), + subscriptionID: try $0.id(), + name: try $0.name(), + newsletterEmailAddress: try $0.newsletterEmail(), + status: try SubscriptionStatus.make(from: $0.status()), + unsubscribeHttpUrl: try $0.unsubscribeHttpUrl(), + unsubscribeMailTo: try $0.unsubscribeMailTo(), + updatedAt: try $0.updatedAt().value, + url: try $0.url() + ) + } + + let selection = Selection { + try $0.on( + subscriptionsError: .init { + QueryResult.error(error: try $0.errorCodes().description) + }, + subscriptionsSuccess: .init { + QueryResult.success(result: try $0.subscriptions(selection: subsciptionSelection.list)) + } + ) + } + + let query = Selection.Query { + try $0.subscriptions(selection: selection) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + return try await withCheckedThrowingContinuation { continuation in + send(query, to: path, headers: headers) { queryResult in + guard let payload = try? queryResult.get() else { + continuation.resume(throwing: BasicError.message(messageText: "network request failed")) + return + } + + switch payload.data { + case let .success(result: result): + continuation.resume(returning: result) + case .error: + continuation.resume(throwing: BasicError.message(messageText: "Subscriptions fetch error")) + } + } + } + } +} + +extension SubscriptionStatus { + static func make(from status: Enums.SubscriptionStatus) -> SubscriptionStatus { + switch status { + case .active: + return .active + case .deleted: + return .deleted + case .unsubscribed: + return .unsubscribed + } + } +} From 0daf97f3124080b9654a534082d29b257f9e2705 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 26 May 2022 08:48:15 -0700 Subject: [PATCH 3/6] create subscription cell --- .../App/Views/Profile/Subscriptions.swift | 114 +++++++++++++++--- .../Sources/Views/Buttons/ButtonStyles.swift | 6 +- 2 files changed, 99 insertions(+), 21 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift index fb0de0b62..b1e90dbce 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift @@ -4,10 +4,11 @@ import SwiftUI import Views @MainActor final class SubscriptionsViewModel: ObservableObject { - @Published var isLoading = false + @Published var isLoading = true @Published var subscriptions = [Subscription]() @Published var popularSubscriptions = [Subscription]() @Published var hasNetworkError = false + @Published var subscriptionIDToCancel: String? func loadSubscriptions(dataService: DataService) async { isLoading = true @@ -20,42 +21,85 @@ import Views isLoading = false } + + func cancelSubscription(dataService _: DataService) { + guard let subscriptionID = subscriptionIDToCancel else { return } + + + + let index = subscriptions.firstIndex { $0.subscriptionID == subscriptionID } + if let index = index { + subscriptions.remove(at: index) + } + } } struct SubscriptionsView: View { @EnvironmentObject var dataService: DataService @StateObject var viewModel = SubscriptionsViewModel() - let footerText = "Describe subscriptions here." + @State private var deleteConfirmationShown = false + @State private var progressViewOpacity = 0.0 var body: some View { - Group { - #if os(iOS) - Form { - innerBody + if viewModel.isLoading { + ProgressView() + .opacity(progressViewOpacity) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) { + progressViewOpacity = 1 + } } - #elseif os(macOS) - List { - innerBody - } - .listStyle(InsetListStyle()) - #endif + .task { await viewModel.loadSubscriptions(dataService: dataService) } + } else if viewModel.hasNetworkError { + VStack { + Text("Sorry, we were unable to retrieve your subscriptions.").multilineTextAlignment(.center) + Button( + action: { Task { await viewModel.loadSubscriptions(dataService: dataService) } }, + label: { Text("Retry") } + ) + .buttonStyle(RoundedRectButtonStyle()) + } + } else { + Group { + #if os(iOS) + Form { + innerBody + } + #elseif os(macOS) + List { + innerBody + } + .listStyle(InsetListStyle()) + #endif + } } - .task { await viewModel.loadSubscriptions(dataService: dataService) } } private var innerBody: some View { Group { ForEach(viewModel.subscriptions, id: \.subscriptionID) { subscription in Button( - action: {}, - label: { Text(subscription.name) } + action: { + #if os(iOS) + UIPasteboard.general.string = subscription.newsletterEmailAddress + #endif + + #if os(macOS) + let pasteBoard = NSPasteboard.general + pasteBoard.clearContents() + pasteBoard.writeObjects([newsletterEmail.newsletterEmailAddress as NSString]) + #endif + + Snackbar.show(message: "Email copied") + }, + label: { SubscriptionCell(subscription: subscription) } ) - .swipeActions(edge: .trailing, allowsFullSwipe: true) { + .swipeActions(edge: .trailing) { Button( role: .destructive, action: { -// itemToRemove = item -// confirmationShown = true + deleteConfirmationShown = true + viewModel.subscriptionIDToCancel = subscription.subscriptionID }, label: { Image(systemName: "trash") @@ -64,6 +108,40 @@ struct SubscriptionsView: View { } } } + .alert("Are you sure you want to cancel this subscription?", isPresented: $deleteConfirmationShown) { + Button("Yes", role: .destructive) { + withAnimation { + viewModel.cancelSubscription(dataService: dataService) + } + } + Button("No", role: .cancel) { + viewModel.subscriptionIDToCancel = nil + } + } .navigationTitle("Subscriptions") } } + +struct SubscriptionCell: View { + let subscription: Subscription + + var body: some View { + VStack { + VStack(alignment: .leading, spacing: 6) { + Text(subscription.name) + .font(.appCallout) + .lineSpacing(1.25) + .foregroundColor(.appGrayTextContrast) + .fixedSize(horizontal: false, vertical: true) + + Text(subscription.newsletterEmailAddress) + .font(.appCaption) + .foregroundColor(.appGrayText) + .lineLimit(1) + } + .multilineTextAlignment(.leading) + .padding(.vertical, 8) + .frame(minHeight: 50) + } + } +} diff --git a/apple/OmnivoreKit/Sources/Views/Buttons/ButtonStyles.swift b/apple/OmnivoreKit/Sources/Views/Buttons/ButtonStyles.swift index 64f72fcb7..6b6e974c6 100644 --- a/apple/OmnivoreKit/Sources/Views/Buttons/ButtonStyles.swift +++ b/apple/OmnivoreKit/Sources/Views/Buttons/ButtonStyles.swift @@ -22,16 +22,16 @@ public struct SolidCapsuleButtonStyle: ButtonStyle { } } -struct RoundedRectButtonStyle: ButtonStyle { +public struct RoundedRectButtonStyle: ButtonStyle { let backgroundColor: Color let textColor: Color - init(color: Color = .appButtonBackground, textColor: Color = .appGrayText) { + public init(color: Color = .appButtonBackground, textColor: Color = .appGrayText) { self.backgroundColor = color self.textColor = textColor } - func makeBody(configuration: Configuration) -> some View { + public func makeBody(configuration: Configuration) -> some View { configuration.label .font(.appBody) .foregroundColor(textColor) From b65b104ce972c89b0b43f083796585cfa4276cef Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 26 May 2022 09:11:24 -0700 Subject: [PATCH 4/6] send delete subscription call to server --- .../App/Views/Profile/Subscriptions.swift | 29 +++++++------ .../Sources/App/Views/RootView/RootView.swift | 3 ++ .../Sources/Models/AppEnvironment.swift | 2 +- .../Mutations/DeleteSubscription.swift | 43 +++++++++++++++++++ 4 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteSubscription.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift index b1e90dbce..b3636d5b5 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift @@ -8,7 +8,7 @@ import Views @Published var subscriptions = [Subscription]() @Published var popularSubscriptions = [Subscription]() @Published var hasNetworkError = false - @Published var subscriptionIDToCancel: String? + @Published var subscriptionNameToCancel: String? func loadSubscriptions(dataService: DataService) async { isLoading = true @@ -22,14 +22,17 @@ import Views isLoading = false } - func cancelSubscription(dataService _: DataService) { - guard let subscriptionID = subscriptionIDToCancel else { return } - - - - let index = subscriptions.firstIndex { $0.subscriptionID == subscriptionID } - if let index = index { - subscriptions.remove(at: index) + func cancelSubscription(dataService: DataService) async { + guard let subscriptionName = subscriptionNameToCancel else { return } + + do { + try await dataService.deleteSubscription(subscriptionName: subscriptionName) + let index = subscriptions.firstIndex { $0.name == subscriptionName } + if let index = index { + subscriptions.remove(at: index) + } + } catch { + appLogger.debug("failed to remove subscription") } } } @@ -99,7 +102,7 @@ struct SubscriptionsView: View { role: .destructive, action: { deleteConfirmationShown = true - viewModel.subscriptionIDToCancel = subscription.subscriptionID + viewModel.subscriptionNameToCancel = subscription.name }, label: { Image(systemName: "trash") @@ -110,12 +113,12 @@ struct SubscriptionsView: View { } .alert("Are you sure you want to cancel this subscription?", isPresented: $deleteConfirmationShown) { Button("Yes", role: .destructive) { - withAnimation { - viewModel.cancelSubscription(dataService: dataService) + Task { + await viewModel.cancelSubscription(dataService: dataService) } } Button("No", role: .cancel) { - viewModel.subscriptionIDToCancel = nil + viewModel.subscriptionNameToCancel = nil } } .navigationTitle("Subscriptions") diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift index 7ba28267a..a946c7eb7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift @@ -1,9 +1,12 @@ import Models +import OSLog import Services import SwiftUI import Utils import Views +let appLogger = Logger(subsystem: "app.omnivore", category: "app-package") + public struct RootView: View { @Environment(\.scenePhase) var scenePhase @StateObject private var viewModel = RootViewModel() diff --git a/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift b/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift index e6929645f..e49cb03f9 100644 --- a/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift +++ b/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift @@ -11,7 +11,7 @@ public enum AppEnvironment: String { public static let initialAppEnvironment: AppEnvironment = { #if DEBUG #if targetEnvironment(simulator) - return .prod // could also return .local here + return .demo // could also return .local here #else return .demo #endif diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteSubscription.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteSubscription.swift new file mode 100644 index 000000000..d8bdf3289 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/DeleteSubscription.swift @@ -0,0 +1,43 @@ +import CoreData +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func deleteSubscription(subscriptionName: String) async throws { + enum MutationResult { + case success(id: String) + case error(errorMessage: String) + } + + let selection = Selection { + try $0.on( + unsubscribeError: .init { .error(errorMessage: (try $0.errorCodes().first ?? .unauthorized).rawValue) }, + unsubscribeSuccess: .init { .success(id: try $0.subscription(selection: Selection.Subscription { try $0.id() })) } + ) + } + + let mutation = Selection.Mutation { + try $0.unsubscribe(name: subscriptionName, selection: selection) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + return try await withCheckedThrowingContinuation { continuation in + send(mutation, to: path, headers: headers) { mutationResult in + guard let payload = try? mutationResult.get() else { + continuation.resume(throwing: BasicError.message(messageText: "network request failed")) + return + } + + switch payload.data { + case .success: + continuation.resume() + case .error: + continuation.resume(throwing: BasicError.message(messageText: "Subscriptions fetch error")) + } + } + } + } +} From 313b7c2ff030521407b4dee45ebe610b0403b76a Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 26 May 2022 12:33:43 -0700 Subject: [PATCH 5/6] update subscription cell UI --- .../App/Views/Profile/Subscriptions.swift | 51 +++++++------------ 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift index b3636d5b5..84afcadd0 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift @@ -81,34 +81,19 @@ struct SubscriptionsView: View { private var innerBody: some View { Group { ForEach(viewModel.subscriptions, id: \.subscriptionID) { subscription in - Button( - action: { - #if os(iOS) - UIPasteboard.general.string = subscription.newsletterEmailAddress - #endif - - #if os(macOS) - let pasteBoard = NSPasteboard.general - pasteBoard.clearContents() - pasteBoard.writeObjects([newsletterEmail.newsletterEmailAddress as NSString]) - #endif - - Snackbar.show(message: "Email copied") - }, - label: { SubscriptionCell(subscription: subscription) } - ) - .swipeActions(edge: .trailing) { - Button( - role: .destructive, - action: { - deleteConfirmationShown = true - viewModel.subscriptionNameToCancel = subscription.name - }, - label: { - Image(systemName: "trash") - } - ) - } + SubscriptionCell(subscription: subscription) + .swipeActions(edge: .trailing) { + Button( + role: .destructive, + action: { + deleteConfirmationShown = true + viewModel.subscriptionNameToCancel = subscription.name + }, + label: { + Image(systemName: "trash") + } + ) + } } } .alert("Are you sure you want to cancel this subscription?", isPresented: $deleteConfirmationShown) { @@ -137,10 +122,12 @@ struct SubscriptionCell: View { .foregroundColor(.appGrayTextContrast) .fixedSize(horizontal: false, vertical: true) - Text(subscription.newsletterEmailAddress) - .font(.appCaption) - .foregroundColor(.appGrayText) - .lineLimit(1) + if let updatedDate = subscription.updatedAt { + Text("Last received: \(updatedDate.formatted())") + .font(.appCaption) + .foregroundColor(.appGrayText) + .fixedSize(horizontal: false, vertical: true) + } } .multilineTextAlignment(.leading) .padding(.vertical, 8) From a854ba88bc879d6ee0f0af73017e75dc511f7213 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 26 May 2022 14:54:47 -0700 Subject: [PATCH 6/6] show snackbar message after unsubscibing --- .../Sources/App/Views/Profile/Subscriptions.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift index 84afcadd0..39808ce52 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/Subscriptions.swift @@ -22,8 +22,8 @@ import Views isLoading = false } - func cancelSubscription(dataService: DataService) async { - guard let subscriptionName = subscriptionNameToCancel else { return } + func cancelSubscription(dataService: DataService) async -> Bool { + guard let subscriptionName = subscriptionNameToCancel else { return false } do { try await dataService.deleteSubscription(subscriptionName: subscriptionName) @@ -31,8 +31,10 @@ import Views if let index = index { subscriptions.remove(at: index) } + return true } catch { appLogger.debug("failed to remove subscription") + return false } } } @@ -99,7 +101,8 @@ struct SubscriptionsView: View { .alert("Are you sure you want to cancel this subscription?", isPresented: $deleteConfirmationShown) { Button("Yes", role: .destructive) { Task { - await viewModel.cancelSubscription(dataService: dataService) + let unsubscribed = await viewModel.cancelSubscription(dataService: dataService) + Snackbar.show(message: unsubscribed ? "Subscription cancelled." : "Could not unsubscribe.") } } Button("No", role: .cancel) {