diff --git a/apple/InfoPlists/Omnivore.plist b/apple/InfoPlists/Omnivore.plist index be36bec66..40d538289 100644 --- a/apple/InfoPlists/Omnivore.plist +++ b/apple/InfoPlists/Omnivore.plist @@ -2,6 +2,10 @@ + NSUserActivityTypes + + app.omnivore.omnivore.openURL + ITSAppUsesNonExemptEncryption BGTaskSchedulerPermittedIdentifiers diff --git a/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift b/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift index 951e34b4f..b867b5f6e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AI/FullScreenDigestView.swift @@ -223,10 +223,11 @@ struct FullScreenDigestView: View { .foregroundColor(Color(hex: "#898989")) .lineLimit(1) } else { - Text("We're building you a new digest") - .font(Font.system(size: 17, weight: .semibold)) - .lineLimit(3) - ProgressView() + HStack { + Spacer() + ProgressView() + Spacer() + } } } .padding(15) diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift index 2a0a32049..a09c6dd88 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift @@ -12,7 +12,6 @@ @EnvironmentObject var dataService: DataService @EnvironmentObject var audioController: AudioController - @Environment(\.colorScheme) private var colorScheme: ColorScheme @Environment(\.dismiss) private var dismiss let delete: (_: NSManagedObjectID) -> Void diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index c19e91c62..e3052b27e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -388,7 +388,6 @@ struct AnimatingCellHeight: AnimatableModifier { } ToolbarItemGroup(placement: .barTrailing) { - if viewModel.appliedFilter?.name == "Deleted" { if viewModel.isEmptyingTrash { ProgressView() @@ -415,6 +414,14 @@ struct AnimatingCellHeight: AnimatableModifier { .buttonStyle(.plain) .padding(.trailing, 4) } +// if #available(iOS 17.0, *), !dataService.featureFlags.digestEnabled, !viewModel.digestHidden { +// Button( +// action: { showDigestConfig = true }, +// label: { Image.tabDigestSelected } +// ) +// .buttonStyle(.plain) +// .padding(.trailing, 4) +// } if #available(iOS 17.0, *), !dataService.featureFlags.digestEnabled, !viewModel.digestHidden { // Give the user an opportunity to enable digest Button( @@ -459,7 +466,7 @@ struct AnimatingCellHeight: AnimatableModifier { } ).buttonStyle(.plain) .padding(.horizontal, UIDevice.isIPad ? 5 : 0) - + Button( action: { searchPresented = true diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 03622c732..ef5a0c495 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -49,6 +49,7 @@ enum LoadingBarStyle { @AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false @AppStorage(UserDefaultKey.stopUsingFollowingPrimer.rawValue) var stopUsingFollowingPrimer = false @AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false + @AppStorage("LibraryTabView::digestHidden") var digestHidden = false @AppStorage(UserDefaultKey.lastVisitedDigestId.rawValue) var lastVisitedDigestId = "" @AppStorage("LibraryTabView::digestHidden") var digestHidden = false @@ -402,7 +403,7 @@ enum LoadingBarStyle { func checkForDigestUpdate(dataService: DataService) async { do { - if let result = try? await dataService.getLatestDigest(timeoutInterval: 2) { + if dataService.featureFlags.digestEnabled, let result = try? await dataService.getLatestDigest(timeoutInterval: 2) { if result.id != lastVisitedDigestId { digestIsUnread = true } diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index 06908b2cd..61bae6e74 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -216,5 +216,8 @@ struct LibraryTabView: View { } selectedTab = "inbox" } + .task { + await dataService.tryUpdateFeatureFlags() + } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift index 3f977b834..6b14f5ff5 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift @@ -30,11 +30,10 @@ import Views init() {} - func submitProfile(name: String, bio: String, authenticator: Authenticator) { + func submitProfile(name: String, authenticator: Authenticator) { let profileOrError = NewUserProfile.make( username: potentialUsername, - name: name, - bio: bio.isEmpty ? nil : bio + name: name ) switch profileOrError { @@ -112,7 +111,6 @@ struct CreateProfileView: View { @StateObject private var viewModel = CreateProfileViewModel() @State private var name = "" - @State private var bio = "" init(userProfile: NewUserProfile) { self.initialUserProfile = userProfile @@ -171,29 +169,11 @@ struct CreateProfileView: View { } .animation(.default, value: 0.35) - VStack(alignment: .leading, spacing: 6) { - Text(LocalText.registrationBio) - .font(.appFootnote) - .foregroundColor(.appGrayText) - TextEditor(text: $bio) - .lineSpacing(6) - .accentColor(.appGraySolid) - .foregroundColor(.appGrayText) - .font(.appBody) - .padding(12) - .background( - RoundedRectangle(cornerRadius: 16) - .strokeBorder(Color.appGrayBorder, lineWidth: 1) - .background(RoundedRectangle(cornerRadius: 16).fill(Color.systemBackground)) - ) - .frame(height: 160) - } - Button( - action: { viewModel.submitProfile(name: name, bio: bio, authenticator: authenticator) }, + action: { viewModel.submitProfile(name: name, authenticator: authenticator) }, label: { Text(viewModel.submitButtonText) } ) - .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) + .buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, textColor: Color.themeDarkGray, width: 300)) if let errorMessage = viewModel.validationErrorMessage { Text(errorMessage) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift index 5006b2981..b5f0a336e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift @@ -45,7 +45,7 @@ struct EmailAuthView: View { var innerBody: some View { ZStack { - Color.appBackground.edgesIgnoringSafeArea(.all) + Color.themeSolidBackground.edgesIgnoringSafeArea(.all) primaryContent .frame(maxWidth: 300) #if os(iOS) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift index 26a9d3d19..74e20465a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift @@ -93,7 +93,7 @@ struct EmailLoginFormView: View { }, label: { Text(LocalText.genericSubmit) } ) - .buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, width: 300)) + .buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, textColor: Color.themeDarkGray, width: 300)) if let loginError = viewModel.loginError { LoginErrorMessageView(loginError: loginError) @@ -186,7 +186,7 @@ struct EmailPendingVerificationView: View { } } ) - .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) + .buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, textColor: Color.themeDarkGray, width: 300)) HStack { Button( diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift index 9e65232a8..79cb97219 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift @@ -191,7 +191,7 @@ struct EmailSignupFormView: View { } } ) - .buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, width: 300)) + .buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, textColor: Color.themeDarkGray, width: 300)) if let loginError = viewModel.loginError { LoginErrorMessageView(loginError: loginError) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift index 13e7f0a71..baa1f25bb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift @@ -50,13 +50,13 @@ struct NewAppleSignupView: View { }, label: { Text(LocalText.genericContinue) } ) - .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) + .buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, textColor: Color.themeDarkGray, width: 300)) Button( action: showProfileEditView, label: { Text(LocalText.registrationChangeUsername) } ) - .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) + .buttonStyle(SolidCapsuleButtonStyle(color: .appCtaYellow, textColor: Color.themeDarkGray, width: 300)) if let loginError = viewModel.loginError { LoginErrorMessageView(loginError: loginError) diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift index 3822faa1e..7260e6bab 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift @@ -37,8 +37,6 @@ public struct RootView: View { Services.scheduleBackgroundFetch() #endif } - }.task { - await viewModel.services.dataService.tryUpdateFeatureFlags() } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 95ce3f22d..fe1842fd9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -44,6 +44,8 @@ struct WebReaderContainerView: View { @State var showExplainSheet = false + @State var handoffActivity: NSUserActivity? + @EnvironmentObject var dataService: DataService @EnvironmentObject var audioController: AudioController @Environment(\.openURL) var openURL @@ -422,6 +424,8 @@ struct WebReaderContainerView: View { .statusBar(hidden: prefersHideStatusBarInReader) #endif .onAppear { + startUserActivity() + dataService.updateLinkReadingProgress( itemID: item.unwrappedID, readingProgress: max(item.readingProgress, 0.1), @@ -437,6 +441,9 @@ struct WebReaderContainerView: View { } } } + .onDisappear { + stopUserActivity() + } .confirmationDialog(linkToOpen?.absoluteString ?? "", isPresented: $displayLinkSheet, titleVisibility: .visible) { Button(action: { @@ -708,7 +715,7 @@ struct WebReaderContainerView: View { func share() { shareActionID = UUID() } - + func printReader() { shareActionID = UUID() } @@ -752,4 +759,23 @@ struct WebReaderContainerView: View { openURL(url) } + + func startUserActivity() { + if let slug = item.slug, + let webpageURL = URL(string: "\(dataService.appEnvironment.webAppBaseURL)/me/\(slug)") { + let activity = NSUserActivity(activityType: "com.omnivore.omnivore.openURL") + activity.title = "Open in browser" + activity.webpageURL = webpageURL + activity.isEligibleForHandoff = true + activity.becomeCurrent() + handoffActivity = activity + } + } + + func stopUserActivity() { + self.handoffActivity = nil + Task { + await NSUserActivity.deleteAllSavedUserActivities() + } + } } diff --git a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift index 77b32451a..cf7a85db7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift @@ -159,6 +159,8 @@ struct WelcomeView: View { var logoView: some View { Image.omnivoreTitleLogo + .renderingMode(.template) + .foregroundColor(Color.appGrayTextContrast) .gesture( TapGesture(count: 2) .onEnded { @@ -231,7 +233,7 @@ struct WelcomeView: View { public var body: some View { ZStack(alignment: viewModel.registrationState == nil ? .leading : .center) { - Color.appBackground + Color.themeSolidBackground .edgesIgnoringSafeArea(.all) .modifier(SizeModifier()) .onPreferenceChange(SizePreferenceKey.self) { @@ -286,7 +288,6 @@ struct WelcomeView: View { } } } - .preferredColorScheme(.light) .task { selectedEnvironment = dataService.appEnvironment } } } diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/NewUserProfile.swift b/apple/OmnivoreKit/Sources/Models/DataModels/NewUserProfile.swift index a13c52bcb..c3dd51777 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/NewUserProfile.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/NewUserProfile.swift @@ -20,13 +20,12 @@ public struct NewUserProfile: Codable { public extension NewUserProfile { static func make( username: String, - name: String, - bio: String? + name: String ) -> Either { let userProfile = NewUserProfile( username: username, name: name, - bio: bio + bio: nil ) if let errorMessage = userProfile.validationErrorMessage { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 0b131f3c7..9794725af 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -300,7 +300,7 @@ public final class DataService: ObservableObject { } return objectID } - + public func tryUpdateFeatureFlags() async { if let features = (try? await fetchViewer())?.enabledFeatures { featureFlags.digestEnabled = features.contains("ai-digest") diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index cd3cf2b2c..56e42410c 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -13273,6 +13273,7 @@ extension Objects { let optInFeature: [String: Unions.OptInFeatureResult] let recommend: [String: Unions.RecommendResult] let recommendHighlights: [String: Unions.RecommendHighlightsResult] + let replyToEmail: [String: Unions.ReplyToEmailResult] let reportItem: [String: Objects.ReportItemResult] let revokeApiKey: [String: Unions.RevokeApiKeyResult] let saveArticleReadingProgress: [String: Unions.SaveArticleReadingProgressResult] @@ -13475,6 +13476,10 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.RecommendHighlightsResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "replyToEmail": + if let value = try container.decode(Unions.ReplyToEmailResult?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "reportItem": if let value = try container.decode(Objects.ReportItemResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -13647,6 +13652,7 @@ extension Objects.Mutation: Decodable { optInFeature = map["optInFeature"] recommend = map["recommend"] recommendHighlights = map["recommendHighlights"] + replyToEmail = map["replyToEmail"] reportItem = map["reportItem"] revokeApiKey = map["revokeApiKey"] saveArticleReadingProgress = map["saveArticleReadingProgress"] @@ -14404,6 +14410,25 @@ extension Fields where TypeLock == Objects.Mutation { } } + func replyToEmail(recentEmailId: String, reply: Enums.AllowedReply, selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "replyToEmail", + arguments: [Argument(name: "recentEmailId", type: "ID!", value: recentEmailId), Argument(name: "reply", type: "AllowedReply!", value: reply)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.replyToEmail[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } + func reportItem(input: InputObjects.ReportItemInput, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "reportItem", @@ -17261,6 +17286,8 @@ extension Objects { let from: [String: String] let html: [String: String] let id: [String: String] + let reply: [String: String] + let replyTo: [String: String] let subject: [String: String] let text: [String: String] let to: [String: String] @@ -17300,6 +17327,14 @@ extension Objects.RecentEmail: Decodable { if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "reply": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "replyTo": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "subject": if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -17330,6 +17365,8 @@ extension Objects.RecentEmail: Decodable { from = map["from"] html = map["html"] id = map["id"] + reply = map["reply"] + replyTo = map["replyTo"] subject = map["subject"] text = map["text"] to = map["to"] @@ -17407,6 +17444,36 @@ extension Fields where TypeLock == Objects.RecentEmail { } } + func reply() throws -> String? { + let field = GraphQLField.leaf( + name: "reply", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.reply[field.alias!] + case .mocking: + return nil + } + } + + func replyTo() throws -> String? { + let field = GraphQLField.leaf( + name: "replyTo", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.replyTo[field.alias!] + case .mocking: + return nil + } + } + func subject() throws -> String { let field = GraphQLField.leaf( name: "subject", @@ -18975,6 +19042,136 @@ extension Selection where TypeLock == Never, Type == Never { typealias ReminderSuccess = Selection } +extension Objects { + struct ReplyToEmailError { + let __typename: TypeName = .replyToEmailError + let errorCodes: [String: [Enums.ReplyToEmailErrorCode]] + + enum TypeName: String, Codable { + case replyToEmailError = "ReplyToEmailError" + } + } +} + +extension Objects.ReplyToEmailError: 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.ReplyToEmailErrorCode]?.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.ReplyToEmailError { + func errorCodes() throws -> [Enums.ReplyToEmailErrorCode] { + 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 ReplyToEmailError = Selection +} + +extension Objects { + struct ReplyToEmailSuccess { + let __typename: TypeName = .replyToEmailSuccess + let success: [String: Bool] + + enum TypeName: String, Codable { + case replyToEmailSuccess = "ReplyToEmailSuccess" + } + } +} + +extension Objects.ReplyToEmailSuccess: 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 "success": + if let value = try container.decode(Bool?.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)." + ) + ) + } + } + + success = map["success"] + } +} + +extension Fields where TypeLock == Objects.ReplyToEmailSuccess { + func success() throws -> Bool { + let field = GraphQLField.leaf( + name: "success", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.success[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return Bool.mockValue + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias ReplyToEmailSuccess = Selection +} + extension Objects { struct ReportItemResult { let __typename: TypeName = .reportItemResult @@ -20445,6 +20642,7 @@ extension Objects { let directionality: [String: Enums.DirectionalityType] let feedContent: [String: String] let folder: [String: String] + let format: [String: String] let highlights: [String: [Objects.Highlight]] let id: [String: String] let image: [String: String] @@ -20545,6 +20743,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 "format": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "highlights": if let value = try container.decode([Objects.Highlight]?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -20699,6 +20901,7 @@ extension Objects.SearchItem: Decodable { directionality = map["directionality"] feedContent = map["feedContent"] folder = map["folder"] + format = map["format"] highlights = map["highlights"] id = map["id"] image = map["image"] @@ -20925,6 +21128,21 @@ extension Fields where TypeLock == Objects.SearchItem { } } + func format() throws -> String? { + let field = GraphQLField.leaf( + name: "format", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.format[field.alias!] + case .mocking: + return nil + } + } + func highlights(selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "highlights", @@ -27785,6 +28003,7 @@ extension Selection where TypeLock == Never, Type == Never { extension Objects { struct UserPersonalization { let __typename: TypeName = .userPersonalization + let digestConfig: [String: String] let fields: [String: String] let fontFamily: [String: String] let fontSize: [String: Int] @@ -27816,6 +28035,10 @@ extension Objects.UserPersonalization: Decodable { let field = GraphQLField.getFieldNameFromAlias(alias) switch field { + case "digestConfig": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "fields": if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -27874,6 +28097,7 @@ extension Objects.UserPersonalization: Decodable { } } + digestConfig = map["digestConfig"] fields = map["fields"] fontFamily = map["fontFamily"] fontSize = map["fontSize"] @@ -27890,6 +28114,21 @@ extension Objects.UserPersonalization: Decodable { } extension Fields where TypeLock == Objects.UserPersonalization { + func digestConfig() throws -> String? { + let field = GraphQLField.leaf( + name: "digestConfig", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.digestConfig[field.alias!] + case .mocking: + return nil + } + } + func fields() throws -> String? { let field = GraphQLField.leaf( name: "fields", @@ -33777,6 +34016,80 @@ extension Selection where TypeLock == Never, Type == Never { typealias ReminderResult = Selection } +extension Unions { + struct ReplyToEmailResult { + let __typename: TypeName + let errorCodes: [String: [Enums.ReplyToEmailErrorCode]] + let success: [String: Bool] + + enum TypeName: String, Codable { + case replyToEmailError = "ReplyToEmailError" + case replyToEmailSuccess = "ReplyToEmailSuccess" + } + } +} + +extension Unions.ReplyToEmailResult: 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.ReplyToEmailErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "success": + if let value = try container.decode(Bool?.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"] + success = map["success"] + } +} + +extension Fields where TypeLock == Unions.ReplyToEmailResult { + func on(replyToEmailError: Selection, replyToEmailSuccess: Selection) throws -> Type { + select([GraphQLField.fragment(type: "ReplyToEmailError", selection: replyToEmailError.selection), GraphQLField.fragment(type: "ReplyToEmailSuccess", selection: replyToEmailSuccess.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .replyToEmailError: + let data = Objects.ReplyToEmailError(errorCodes: data.errorCodes) + return try replyToEmailError.decode(data: data) + case .replyToEmailSuccess: + let data = Objects.ReplyToEmailSuccess(success: data.success) + return try replyToEmailSuccess.decode(data: data) + } + case .mocking: + return replyToEmailError.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias ReplyToEmailResult = Selection +} + extension Unions { struct RevokeApiKeyResult { let __typename: TypeName @@ -37206,6 +37519,19 @@ extension Enums { } } +extension Enums { + /// AllowedReply + enum AllowedReply: String, CaseIterable, Codable { + case confirm = "CONFIRM" + + case okay = "OKAY" + + case subscribe = "SUBSCRIBE" + + case yes = "YES" + } +} + extension Enums { /// ApiKeysErrorCode enum ApiKeysErrorCode: String, CaseIterable, Codable { @@ -37911,6 +38237,8 @@ extension Enums { enum OptInFeatureErrorCode: String, CaseIterable, Codable { case badRequest = "BAD_REQUEST" + case ineligible = "INELIGIBLE" + case notFound = "NOT_FOUND" } } @@ -38008,6 +38336,13 @@ extension Enums { } } +extension Enums { + /// ReplyToEmailErrorCode + enum ReplyToEmailErrorCode: String, CaseIterable, Codable { + case unauthorized = "UNAUTHORIZED" + } +} + extension Enums { /// ReportType enum ReportType: String, CaseIterable, Codable { @@ -40044,6 +40379,8 @@ extension InputObjects { extension InputObjects { struct SetUserPersonalizationInput: Encodable, Hashable { + var digestConfig: OptionalArgument = .absent() + var fields: OptionalArgument = .absent() var fontFamily: OptionalArgument = .absent() @@ -40068,6 +40405,7 @@ extension InputObjects { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + if digestConfig.hasValue { try container.encode(digestConfig, forKey: .digestConfig) } if fields.hasValue { try container.encode(fields, forKey: .fields) } if fontFamily.hasValue { try container.encode(fontFamily, forKey: .fontFamily) } if fontSize.hasValue { try container.encode(fontSize, forKey: .fontSize) } @@ -40082,6 +40420,7 @@ extension InputObjects { } enum CodingKeys: String, CodingKey { + case digestConfig case fields case fontFamily case fontSize diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/OptIntoFeature.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/OptIntoFeature.swift index 19a9f6060..1bf387bef 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/OptIntoFeature.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/OptIntoFeature.swift @@ -15,6 +15,10 @@ public struct Feature { public let granted: Bool } +public enum IneligibleError: Error { + case message(messageText: String) +} + public extension DataService { func optInFeature(name: String) async throws -> Feature? { enum MutationResult { @@ -51,6 +55,9 @@ public extension DataService { case let .success(feature: feature): continuation.resume(returning: feature) case let .error(errorCode: errorCode): + if errorCode == .ineligible { + continuation.resume(throwing: IneligibleError.message(messageText: "You are not eligible for this feature.")) + } continuation.resume(throwing: BasicError.message(messageText: errorCode.rawValue)) } } diff --git a/apple/OmnivoreKit/Sources/Views/Buttons/AppleSignInButton.swift b/apple/OmnivoreKit/Sources/Views/Buttons/AppleSignInButton.swift index a4e206e52..ea6b33c6a 100644 --- a/apple/OmnivoreKit/Sources/Views/Buttons/AppleSignInButton.swift +++ b/apple/OmnivoreKit/Sources/Views/Buttons/AppleSignInButton.swift @@ -21,6 +21,5 @@ public struct AppleSignInButton: View { .frame(height: 54) .frame(maxWidth: 300) .cornerRadius(8) - .signInWithAppleButtonStyle(.white) } } diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/100.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/100.png index b05446cc4..a77b6d27d 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/100.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/1024.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/1024.png index 1d730a207..1ea66e520 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/1024.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/114.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/114.png index c5b1a9ec7..b0bf59467 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/114.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/120.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/120.png index 2d29a454a..b637c83f9 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/120.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/144.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/144.png index 1e85b1eba..d36c7cb85 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/144.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/152.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/152.png index d67375bc0..f35d31dbd 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/152.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/167.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/167.png index 112c03700..7673419cd 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/167.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/180.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/180.png index bd895c1c6..b870712b2 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/180.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/20.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/20.png index 9d32728b4..08c152eb7 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/20.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/29.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/29.png index 1f335b31e..3cb71eda1 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/29.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/40.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/40.png index 8d2bd163f..927bd6b5f 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/40.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/50.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/50.png index 6e9a82ed0..27334e579 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/50.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/57.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/57.png index 5bb947b38..91c1cc95d 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/57.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/58.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/58.png index 3430925c2..582468935 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/58.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/60.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/60.png index 42ed00f69..62acfc344 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/60.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/72.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/72.png index 44170f0e8..f4db76e15 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/72.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/76.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/76.png index bd163096c..82d82dfa0 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/76.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/80.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/80.png index 2ec51e0bb..1269eaa79 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/80.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/apple/Resources/Assets.xcassets/AppIcon.appiconset/87.png b/apple/Resources/Assets.xcassets/AppIcon.appiconset/87.png index f32f5abae..b64e7c375 100644 Binary files a/apple/Resources/Assets.xcassets/AppIcon.appiconset/87.png and b/apple/Resources/Assets.xcassets/AppIcon.appiconset/87.png differ