Merge pull request #714 from omnivore-app/subscriptions-modal

Subscriptions Screen [iOS]
This commit is contained in:
Satindar Dhillon 2022-05-26 15:12:19 -07:00 committed by GitHub
commit a9b70e12e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1085 additions and 3 deletions

View file

@ -78,6 +78,10 @@ struct ProfileView: View {
NavigationLink(destination: NewsletterEmailsView()) {
Text("Emails")
}
NavigationLink(destination: SubscriptionsView()) {
Text("Subscriptions")
}
}
Section {

View file

@ -0,0 +1,140 @@
import Models
import Services
import SwiftUI
import Views
@MainActor final class SubscriptionsViewModel: ObservableObject {
@Published var isLoading = true
@Published var subscriptions = [Subscription]()
@Published var popularSubscriptions = [Subscription]()
@Published var hasNetworkError = false
@Published var subscriptionNameToCancel: String?
func loadSubscriptions(dataService: DataService) async {
isLoading = true
do {
subscriptions = try await dataService.subscriptions()
} catch {
hasNetworkError = true
}
isLoading = false
}
func cancelSubscription(dataService: DataService) async -> Bool {
guard let subscriptionName = subscriptionNameToCancel else { return false }
do {
try await dataService.deleteSubscription(subscriptionName: subscriptionName)
let index = subscriptions.firstIndex { $0.name == subscriptionName }
if let index = index {
subscriptions.remove(at: index)
}
return true
} catch {
appLogger.debug("failed to remove subscription")
return false
}
}
}
struct SubscriptionsView: View {
@EnvironmentObject var dataService: DataService
@StateObject var viewModel = SubscriptionsViewModel()
@State private var deleteConfirmationShown = false
@State private var progressViewOpacity = 0.0
var body: some View {
if viewModel.isLoading {
ProgressView()
.opacity(progressViewOpacity)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) {
progressViewOpacity = 1
}
}
.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
}
}
}
private var innerBody: some View {
Group {
ForEach(viewModel.subscriptions, id: \.subscriptionID) { subscription in
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) {
Button("Yes", role: .destructive) {
Task {
let unsubscribed = await viewModel.cancelSubscription(dataService: dataService)
Snackbar.show(message: unsubscribed ? "Subscription cancelled." : "Could not unsubscribe.")
}
}
Button("No", role: .cancel) {
viewModel.subscriptionNameToCancel = 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)
if let updatedDate = subscription.updatedAt {
Text("Last received: \(updatedDate.formatted())")
.font(.appCaption)
.foregroundColor(.appGrayText)
.fixedSize(horizontal: false, vertical: true)
}
}
.multilineTextAlignment(.leading)
.padding(.vertical, 8)
.frame(minHeight: 50)
}
}
}

View file

@ -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()

View file

@ -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
}

View file

@ -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<T> = Selection<T, Objects.AddPopularReadError>
}
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<T> = Selection<T, Objects.AddPopularReadSuccess>
}
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<Type>(name: String, selection: Selection<Type, Unions.AddPopularReadResult>) 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<Type>(input: InputObjects.CreateArticleInput, selection: Selection<Type, Unions.CreateArticleResult>) throws -> Type {
let field = GraphQLField.composite(
name: "createArticle",
@ -7634,6 +7801,25 @@ extension Fields where TypeLock == Objects.Mutation {
}
}
func subscribe<Type>(name: String, selection: Selection<Type, Unions.SubscribeResult>) 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<Type>(name: String, selection: Selection<Type, Unions.UnsubscribeResult>) throws -> Type {
let field = GraphQLField.composite(
name: "unsubscribe",
@ -7729,6 +7915,25 @@ extension Fields where TypeLock == Objects.Mutation {
}
}
func updatePage<Type>(input: InputObjects.UpdatePageInput, selection: Selection<Type, Unions.UpdatePageResult>) 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<Type>(input: InputObjects.UpdateReminderInput, selection: Selection<Type, Unions.UpdateReminderResult>) 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<T> = Selection<T, Objects.SignupSuccess>
}
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<T> = Selection<T, Objects.SubscribeError>
}
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<Type>(selection: Selection<Type, [Objects.Subscription]>) 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<T> = Selection<T, Objects.SubscribeSuccess>
}
extension Objects {
struct Subscription {
let __typename: TypeName = .subscription
@ -13386,6 +13743,137 @@ extension Selection where TypeLock == Never, Type == Never {
typealias UpdateLinkShareInfoSuccess<T> = Selection<T, Objects.UpdateLinkShareInfoSuccess>
}
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<T> = Selection<T, Objects.UpdatePageError>
}
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<Type>(selection: Selection<Type, Objects.Page>) 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<T> = Selection<T, Objects.UpdatePageSuccess>
}
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<Type>(addPopularReadError: Selection<Type, Objects.AddPopularReadError>, addPopularReadSuccess: Selection<Type, Objects.AddPopularReadSuccess>) 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<T> = Selection<T, Unions.AddPopularReadResult>
}
extension Unions {
struct ArchiveLinkResult {
let __typename: TypeName
@ -18065,6 +18627,80 @@ extension Selection where TypeLock == Never, Type == Never {
typealias SignupResult<T> = Selection<T, Unions.SignupResult>
}
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<Type>(subscribeError: Selection<Type, Objects.SubscribeError>, subscribeSuccess: Selection<Type, Objects.SubscribeSuccess>) 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<T> = Selection<T, Unions.SubscribeResult>
}
extension Unions {
struct SubscriptionsResult {
let __typename: TypeName
@ -18509,6 +19145,80 @@ extension Selection where TypeLock == Never, Type == Never {
typealias UpdateLinkShareInfoResult<T> = Selection<T, Unions.UpdateLinkShareInfoResult>
}
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<Type>(updatePageError: Selection<Type, Objects.UpdatePageError>, updatePageSuccess: Selection<Type, Objects.UpdatePageSuccess>) 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<T> = Selection<T, Unions.UpdatePageResult>
}
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<String> = .absent()
var pageId: String
var title: OptionalArgument<String> = .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

View file

@ -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<MutationResult, Unions.UnsubscribeResult> {
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"))
}
}
}
}
}

View file

@ -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<QueryResult, Unions.SubscriptionsResult> {
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
}
}
}

View file

@ -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)