Created
February 1, 2024 14:08
-
-
Save saroar/1e813c47b9d832eb16378efa972c7a14 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ComposableArchitecture | |
import ComposableUserNotifications | |
import Foundation | |
import SettingsFeature | |
import RemoteNotificationsClient | |
import LPGSharedModels | |
import UIKit | |
import os | |
import NotificationHelpers | |
import BSON | |
@Reducer | |
public struct AppDelegateReducer { | |
public typealias State = UserSettings | |
public enum Action: Equatable { | |
case didFinishLaunching | |
case didRegisterForRemoteNotifications(TaskResult<Data>) | |
case userNotifications(UserNotificationClient.DelegateEvent) | |
case userSettingsLoaded(TaskResult<UserSettings>) | |
case deviceResponse(TaskResult<DeviceInOutPut>) | |
case getNotificationSettings | |
case createOrUpdate(deviceToken: Data) | |
} | |
@Dependency(\.apiClient) var apiClient | |
@Dependency(\.build.number) var buildNumber | |
@Dependency(\.remoteNotifications) var remoteNotifications | |
@Dependency(\.applicationClient.setUserInterfaceStyle) var setUserInterfaceStyle | |
@Dependency(\.userNotifications) var userNotifications | |
public init() {} | |
public func reduce(into state: inout State, action: Action) -> Effect<Action> { | |
switch action { | |
case .didFinishLaunching: | |
return .run { send in | |
await withThrowingTaskGroup(of: Void.self) { group in | |
group.addTask { | |
for await event in self.userNotifications.delegate() { | |
await send(.userNotifications(event)) | |
} | |
} | |
group.addTask { | |
let settings = await self.userNotifications.getNotificationSettings() | |
switch settings.authorizationStatus { | |
case .authorized: | |
guard | |
try await self.userNotifications.requestAuthorization([.alert, .badge, .sound]) | |
else { return } | |
case .notDetermined, .provisional: | |
guard try await self.userNotifications.requestAuthorization(.provisional) | |
else { return } | |
default: | |
return | |
} | |
} | |
group.addTask { | |
await registerForRemoteNotificationsAsync( | |
remoteNotifications: self.remoteNotifications, | |
userNotifications: self.userNotifications | |
) | |
} | |
} | |
} | |
case let .didRegisterForRemoteNotifications(.success(data)): | |
return .run { send in | |
await send(.getNotificationSettings) | |
await send(.createOrUpdate(deviceToken: data)) | |
} | |
case .didRegisterForRemoteNotifications(.failure): | |
return .none | |
case .getNotificationSettings: | |
logger.info("\(#line) run getNotificationSettings") | |
return .run { _ in | |
_ = await self.userNotifications.getNotificationSettings() | |
} | |
case let .createOrUpdate(deviceToken: data): | |
let identifierForVendor = UIDevice.current.identifierForVendor?.uuidString | |
let token = data.toHexString | |
let device = DeviceInOutPut( | |
identifierForVendor: identifierForVendor, | |
name: UIDevice.current.name, | |
model: UIDevice.current.model, | |
osVersion: UIDevice.current.systemVersion, | |
pushToken: token, | |
voipToken: "" | |
) | |
return .run { send in | |
await send(.deviceResponse( | |
await TaskResult { | |
try await apiClient.request( | |
for: .authEngine(.devices(.createOrUpdate(input: device))), | |
as: DeviceInOutPut.self, | |
decoder: .iso8601 | |
) | |
} | |
)) | |
} | |
case let .userNotifications(.willPresentNotification(_, completionHandler)): | |
return .run { _ in completionHandler(.banner) } | |
case .userNotifications: | |
return .none | |
case let .userSettingsLoaded(result): | |
state = (try? result.value) ?? state | |
return .run { [state] _ in | |
async let setUI: Void = | |
await self.setUserInterfaceStyle(state.colorScheme.userInterfaceStyle) | |
_ = await setUI | |
} | |
case .deviceResponse(.success): | |
logger.info("\(#line) deviceResponse success") | |
return .none | |
case .deviceResponse(.failure(let error)): | |
logger.error("\(#line) deviceResponse error \(error.localizedDescription)") | |
return .none | |
} | |
} | |
} | |
public let logger = Logger(subsystem: "com.learnplaygrow", category: "appDelegate.reducer") | |
------------------------------------- | |
import AuthenticationView | |
import ComposableArchitecture | |
import ConversationsView | |
import ProfileView | |
import SwiftUI | |
import UserDefaultsClient | |
import KeychainClient | |
import LPGSharedModels | |
import LocationReducer | |
import NotificationHelpers | |
import AuthenticationCore | |
import TabFeature | |
@Reducer | |
public struct AppReducer { | |
@Reducer | |
public struct Destination { | |
public enum State: Equatable { | |
case login(Login.State) | |
case tab(TabReducer.State) | |
} | |
@CasePathable | |
public enum Action: Equatable { | |
case login(Login.Action) | |
case tab(TabReducer.Action) | |
} | |
public var body: some Reducer<State, Action> { | |
Scope(state: \.login, action: \.login) { | |
Login() | |
} | |
Scope(state: \.tab, action: \.tab) { | |
TabReducer() | |
} | |
} | |
} | |
@ObservableState | |
public struct State: Equatable { | |
@Presents public var destination: Destination.State? = .tab(.init()) | |
} | |
@CasePathable | |
public enum Action { | |
case destination(PresentationAction<Destination.Action>) | |
case onAppear | |
case appDelegate(AppDelegateReducer.Action) | |
case didChangeScenePhase(ScenePhase) | |
} | |
@Dependency(\.userDefaults) var userDefaults | |
@Dependency(\.userNotifications) var userNotifications | |
@Dependency(\.remoteNotifications) var remoteNotifications | |
@Dependency(\.mainRunLoop) var mainRunLoop | |
@Dependency(\.keychainClient) var keychainClient | |
@Dependency(\.build) var build | |
public init() {} | |
public var body: some Reducer<State, Action> { | |
Scope( | |
state: \.destination?.tab?.settings.userSettings, | |
action: \.appDelegate | |
) { | |
AppDelegateReducer() | |
} | |
Reduce { state, action in | |
switch action { | |
case .onAppear: | |
let isAuthorized = userDefaults.boolForKey(UserDefaultKey.isAuthorized.rawValue) == true | |
let isAskPermissionCompleted = userDefaults.boolForKey(UserDefaultKey.isAskPermissionCompleted.rawValue) == true | |
if isAuthorized && isAskPermissionCompleted { | |
return .none | |
} else { | |
state.destination = .login(.init()) //Login.State() | |
return .none | |
} | |
case .destination(.presented(.login(.verificationResponse(.success)))): | |
// state.destination = .login() //.login(.register(.init())) | |
return .none | |
case .destination(.presented(.login(.moveToTableView))): | |
// state.loginState = nil | |
// state.tabState = .init() | |
return .none | |
case .destination(.presented(.tab(.settings(.logOutButtonTapped)))): | |
state.destination = .login(.init()) | |
return .run(priority: .background) { _ in | |
await withThrowingTaskGroup(of: Void.self) { group in | |
group.addTask { | |
await userDefaults.setBool( | |
false, | |
UserDefaultKey.isAuthorized.rawValue | |
) | |
} | |
group.addTask { | |
try await keychainClient.logout() | |
} | |
} | |
} | |
case let .appDelegate(.userNotifications(.didReceiveResponse(_, completionHandler))): | |
return .run { _ in completionHandler() } | |
case .appDelegate: | |
return .none | |
case .didChangeScenePhase(.active): | |
return .run { send in | |
// await send(.tab(.connect)) | |
} | |
case .didChangeScenePhase(.background): | |
return .run { send in | |
// await send(.tab(.disConnect)) | |
} | |
case .didChangeScenePhase: | |
return .none | |
case .destination: | |
return .none | |
} | |
} | |
} | |
} | |
public struct AppView: View { | |
@Environment(\.scenePhase) private var scenePhase | |
public let store: StoreOf<AppReducer> | |
public init(store: StoreOf<AppReducer>) { | |
self.store = store | |
} | |
public var body: some View { | |
WithPerceptionTracking { | |
ZStack { | |
// if store.loginState == nil { | |
// NavigationView { | |
// TabBarView( | |
// store: self.store.scope(state: \.tabState, action: \.tab) | |
// ) | |
// } | |
// .navigationViewStyle(.stack) | |
// } else { | |
// if let loginStore = store.scope(state: \.loginState, action: \.login) { | |
// AuthenticationView(store: loginStore) | |
// } | |
// } | |
} | |
} | |
.onAppear { | |
store.send(.onAppear) | |
} | |
} | |
} | |
#if DEBUG | |
struct AppView_Previews: PreviewProvider { | |
static var previews: some View { | |
AppView(store: .init(initialState: AppReducer.State()) { | |
AppReducer() | |
}) | |
} | |
} | |
#endif | |
--------------------------------- | |
import os | |
import UIKit | |
import Build | |
import Foundation | |
import KeychainClient | |
import LocationReducer | |
import LPGSharedModels | |
import UserDefaultsClient | |
import ComposableStoreKit | |
import UIApplicationClient | |
import FoundationExtension | |
import NotificationHelpers | |
import ComposableArchitecture | |
import ComposableUserNotifications | |
@Reducer | |
public struct Settings { | |
@ObservableState | |
public struct State: Equatable { | |
public init( | |
alert: AlertState<Settings.Action>? = nil, | |
currentUser: UserOutput = .withFirstName, | |
buildNumber: Build.Number? = nil, | |
enableNotifications: Bool = false, | |
userNotificationSettings: UserNotificationClient.Notification.Settings? = nil, | |
userSettings: UserSettings = UserSettings(), | |
locationState: LocationReducer.State = .init(), | |
distanceState: Distance.State = .init() | |
) { | |
self.alert = alert | |
self.currentUser = currentUser | |
self.buildNumber = buildNumber | |
self.enableNotifications = enableNotifications | |
self.userNotificationSettings = userNotificationSettings | |
self.userSettings = userSettings | |
self.locationState = locationState | |
self.distanceState = distanceState | |
} | |
@Presents public var alert: AlertState<Action>? | |
public var currentUser: UserOutput = .withFirstName | |
public var buildNumber: Build.Number? | |
public var enableNotifications: Bool | |
public var userNotificationSettings: UserNotificationClient.Notification.Settings? | |
public var distanceState: Distance.State | |
public var userSettings: UserSettings | |
public var locationState: LocationReducer.State | |
} | |
public enum Action: BindableAction, Equatable { | |
case binding(BindingAction<State>) | |
case onAppear | |
case didBecomeActive | |
case openSettingButtonTapped | |
case userNotificationAuthorizationResponse(TaskResult<Bool>) | |
case userNotificationSettingsResponse(UserNotificationClient.Notification.Settings) | |
case leaveUsAReviewButtonTapped | |
case reportABugButtonTapped | |
case logOutButtonTapped | |
case location(LocationReducer.Action) | |
case distance(Distance.Action) | |
} | |
@Dependency(\.applicationClient) var applicationClient | |
@Dependency(\.mainQueue) var mainQueue | |
@Dependency(\.userNotifications) var userNotifications | |
@Dependency(\.userDefaults) var userDefaults | |
@Dependency(\.build) var build | |
@Dependency(\.keychainClient) var keychainClient | |
@Dependency(\.remoteNotifications.unregister) var unRegisterForRemoteNotifications | |
public init() {} | |
public var body: some Reducer<State, Action> { | |
CombineReducers { | |
BindingReducer() | |
Scope(state: \.distanceState, action: /Action.distance) { | |
Distance() | |
} | |
Reduce { state, action in | |
switch action { | |
case .binding: | |
return .none | |
case .binding(\.enableNotifications): | |
guard | |
state.enableNotifications, | |
let userNotificationSettings = state.userNotificationSettings | |
else { | |
// TODO: API request to opt out of all notifications | |
state.enableNotifications = false | |
return .none | |
} | |
state.userNotificationSettings?.authorizationStatus = state.enableNotifications == true ? .authorized : .denied | |
switch userNotificationSettings.authorizationStatus { | |
case .notDetermined, .provisional: | |
state.enableNotifications = true | |
return .run { send in | |
await send(.userNotificationAuthorizationResponse( | |
TaskResult { | |
try await self.userNotifications.requestAuthorization([.alert, .badge, .sound]) | |
} | |
)) | |
} | |
.animation() | |
case .denied: | |
state.alert = .userNotificationAuthorizationDenied | |
state.enableNotifications = false | |
return .none | |
case .authorized: | |
state.enableNotifications = true | |
return .run { send in | |
await send(.userNotificationAuthorizationResponse(.success(true))) | |
} | |
case .ephemeral: | |
state.enableNotifications = true | |
return .none | |
@unknown default: | |
return .none | |
} | |
case .onAppear: | |
state.buildNumber = self.build.number() | |
do { | |
state.currentUser = try keychainClient.readCodable(.user, self.build.identifier(), UserOutput.self) | |
} catch { | |
// fatalError("Do soemthing from SettingsFeature!") | |
logger.error("cant get current user from keychainClient ") | |
} | |
return .merge( | |
.run { send in | |
async let settingsResponse: Void = send( | |
.userNotificationSettingsResponse( | |
self.userNotifications.getNotificationSettings() | |
), | |
animation: .default | |
) | |
_ = await settingsResponse | |
} | |
// NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) | |
// .map { _ in .didBecomeActive } | |
// .eraseToEffect() | |
) | |
case .openSettingButtonTapped: | |
return .run { _ in | |
guard | |
let url = await URL(string: self.applicationClient.openSettingsURLString()) | |
else { return } | |
_ = await self.applicationClient.open(url, [:]) | |
} | |
case let .userNotificationAuthorizationResponse(.success(granted)): | |
state.enableNotifications = granted | |
return granted | |
? .run { _ in | |
await self.unRegisterForRemoteNotifications() | |
}// .fireAndForget { await self.registerForRemoteNotifications() } | |
: .none | |
case .userNotificationAuthorizationResponse: | |
return .none | |
case let .userNotificationSettingsResponse(settings): | |
state.userNotificationSettings = settings | |
state.enableNotifications = settings.authorizationStatus == .authorized | |
return .none | |
case .leaveUsAReviewButtonTapped: | |
return .run { _ in | |
_ = await self.applicationClient.open(appStoreReviewUrl, [:]) | |
} | |
case .didBecomeActive: | |
return .run { send in | |
await send(.userNotificationSettingsResponse( | |
self.userNotifications.getNotificationSettings() | |
)) | |
} | |
case .reportABugButtonTapped: | |
return .run { [currentUser = state.currentUser] _ in | |
let currentUser = currentUser | |
var components = URLComponents() | |
components.scheme = "mailto" | |
components.path = "[email protected]" | |
components.queryItems = [ | |
URLQueryItem(name: "subject", value: "I found a bug in Addame IOS App"), | |
URLQueryItem( | |
name: "body", | |
value: """ | |
--- | |
Build: \(self.build.number()) (\(self.build.gitSha())) | |
\(currentUser.id.hexString) | |
""" | |
) | |
] | |
_ = await self.applicationClient.open(components.url!, [:]) | |
} | |
case .location(_): | |
return .none | |
case .distance(_): | |
return .none | |
case .logOutButtonTapped: | |
return .none | |
} | |
} | |
} | |
} | |
private var appStoreReviewUrl: URL { | |
URL( | |
string: "https://itunes.apple.com/us/app/apple-store/id1619504857?mt=8&action=write-review" | |
)! | |
} | |
} | |
extension AlertState where Action == Settings.Action { | |
static let userNotificationAuthorizationDenied = Self { | |
TextState("Permission Denied") | |
} actions: { | |
ButtonState(role: .destructive, action: .openSettingButtonTapped) { | |
TextState("Open Settings") | |
} | |
ButtonState(role: .cancel) { | |
TextState("Ok") | |
} | |
} message: { | |
TextState("Turn on notifications in iOS settings.") | |
} | |
static let restoredPurchasesFailed = Self( | |
title: .init("Error"), | |
message: .init("We couldn’t restore purchases, please try again."), | |
dismissButton: .default(.init("Ok")) | |
) | |
static let noRestoredPurchases = Self( | |
title: .init("No Purchases"), | |
message: .init("No purchases were found to restore."), | |
dismissButton: .default(.init("Ok")) | |
) | |
} | |
public let logger = Logger(subsystem: "com.addame.AddaMeIOS", category: "settins.reducer") | |
-------------------------------------- | |
public struct UserSettings: Codable, Equatable { | |
public var colorScheme: ColorScheme | |
public enum ColorScheme: String, CaseIterable, Codable { | |
case dark | |
case light | |
case system | |
public var userInterfaceStyle: UIUserInterfaceStyle { | |
switch self { | |
case .dark: | |
return .dark | |
case .light: | |
return .light | |
case .system: | |
return .unspecified | |
} | |
} | |
} | |
public init(colorScheme: ColorScheme = .system) { | |
self.colorScheme = colorScheme | |
} | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
self.colorScheme = (try? container.decode(ColorScheme.self, forKey: .colorScheme)) ?? .system | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment