Skip to content

Instantly share code, notes, and snippets.

@saroar
Created February 1, 2024 14:08
Show Gist options
  • Save saroar/1e813c47b9d832eb16378efa972c7a14 to your computer and use it in GitHub Desktop.
Save saroar/1e813c47b9d832eb16378efa972c7a14 to your computer and use it in GitHub Desktop.
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