Created
February 3, 2024 13:33
-
-
Save saroar/daa1ee445871b9e309cfb170d05b5f96 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
// | |
// LearnPlayGrowApp.swift | |
// LearnPlayGrow | |
// | |
// Created by Saroar Khandoker on 20.03.2023. | |
// | |
import UIKit | |
import SwiftUI | |
import AppView | |
import AppFeature | |
import ComposableArchitecture | |
final class AppDelegate: NSObject, UIApplicationDelegate { | |
let store = Store(initialState: AppReducer.State()) { | |
AppReducer()._printChanges() | |
} | |
func application( | |
_ application: UIApplication, | |
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil | |
) -> Bool { | |
self.store.send(.appDelegate(.didFinishLaunching)) | |
return true | |
} | |
func application( | |
_ application: UIApplication, | |
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data | |
) { | |
self.store.send(.appDelegate(.didRegisterForRemoteNotifications(.success(deviceToken)))) | |
} | |
func application( | |
_ application: UIApplication, | |
didFailToRegisterForRemoteNotificationsWithError error: Error | |
) { | |
self.store.send(.appDelegate(.didRegisterForRemoteNotifications(.failure(error)))) | |
} | |
} | |
@main | |
struct LearnPlayGrowApp: App { | |
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate | |
@Environment(\.scenePhase) private var scenePhase | |
var body: some Scene { | |
WindowGroup { | |
AppView(store: self.appDelegate.store) | |
} | |
.onChange(of: self.scenePhase) { | |
self.appDelegate.store.send(.didChangeScenePhase($0)) | |
} | |
} | |
} | |
@Reducer | |
public struct AppDelegateReducer { | |
public struct State: Equatable { | |
public init() {} | |
} | |
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 .userSettingsLoaded: | |
return .none | |
// var newState = result | |
// return .run { _ in | |
// async let setUI: Void = | |
// await self.setUserInterfaceStyle(newState.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.addame.AddaMeIOS", category: "appDelegate.reducer") | |
@Reducer | |
public struct AppReducer { | |
@ObservableState | |
public struct State: Equatable { | |
public init( | |
startup: Startup.State = .login(.init()), | |
appDelegate: AppDelegateReducer.State = .init() | |
) { | |
self.startup = startup | |
self.appDelegate = appDelegate | |
} | |
public var startup: Startup.State | |
public var appDelegate: AppDelegateReducer.State | |
} | |
@CasePathable | |
public enum Action { | |
case startup(Startup.Action) | |
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: \.appDelegate, action: /Action.appDelegate) { | |
AppDelegateReducer() | |
} | |
Scope(state: \.startup, action: \.startup) { | |
Startup() | |
} | |
Reduce(self.core) | |
} | |
func core(state: inout State, action: Action) -> Effect<Action> { | |
switch action { | |
case .startup: | |
return .none | |
case .appDelegate: | |
return .none | |
case .didChangeScenePhase: | |
return .none | |
} | |
} | |
} | |
import SwiftUI | |
import TabFeature | |
import ProfileView | |
import KeychainClient | |
import LocationReducer | |
import LPGSharedModels | |
import ConversationsView | |
import AuthenticationView | |
import UserDefaultsClient | |
import AuthenticationCore | |
import NotificationHelpers | |
import RegisterFormFeature | |
import ComposableArchitecture | |
@Reducer | |
public struct Startup { | |
@ObservableState | |
public enum State: Equatable { | |
case login(Login.State) | |
case register(RegisterFormReducer.State) | |
case tabbar(TabReducer.State) | |
} | |
public enum Action: Equatable { | |
case onAppear | |
case login(Login.Action) | |
case register(RegisterFormReducer.Action) | |
case tabbar(TabReducer.Action) | |
case move(Move) | |
public enum Move { | |
case login, register, tabbar | |
} | |
} | |
public init() {} | |
@Dependency(\.userDefaults) var userDefaults | |
public var body: some Reducer<State, Action> { | |
Scope(state: \.login, action: \.login) { | |
Login() | |
} | |
Scope(state: \.register, action: \.register) { | |
RegisterFormReducer() | |
} | |
Scope(state: \.tabbar, action: \.tabbar) { | |
TabReducer() | |
} | |
Reduce(self.core) | |
} | |
func core(state: inout State, action: Action) -> Effect<Action> { | |
switch action { | |
case .onAppear: | |
let isAuthorized = userDefaults.boolForKey(UserDefaultKey.isAuthorized.rawValue) == true | |
let isAskPermissionCompleted = userDefaults.boolForKey(UserDefaultKey.isAskPermissionCompleted.rawValue) == true | |
if isAuthorized && isAskPermissionCompleted { | |
return .run { send in | |
await send(.move(.tabbar)) | |
} | |
} else if isAuthorized && !isAskPermissionCompleted { | |
return .run { send in | |
await send(.move(.register)) | |
} | |
} else { | |
return .run { send in | |
await send(.move(.login)) | |
} | |
} | |
case .login(.moveToPermissionsFrom): | |
state = .register(.init()) | |
return .none | |
case .login: | |
state = .login(.init()) | |
return .none | |
case .register(.moveToTabberView): | |
state = .tabbar(.init()) | |
return .none | |
case .register: | |
state = .register(.init()) | |
return .none | |
case .tabbar: | |
state = .tabbar(.init()) | |
return .none | |
case .move: | |
return .none | |
} | |
} | |
} | |
import SwiftUI | |
public struct StartupView: View { | |
public let store: StoreOf<Startup> | |
public init(store: StoreOf<Startup>) { | |
self.store = store | |
} | |
public var body: some View { | |
WithPerceptionTracking { | |
ZStack(alignment: .center) { | |
switch self.store.state { | |
case .login: | |
if let store = self.store.scope(state: \.login, action: \.login) { | |
AuthenticationView(store: store) | |
} | |
case .register: | |
if let store = self.store.scope(state: \.register, action: \.register) { | |
RegisterFormView(store: store) | |
} | |
case .tabbar: | |
if let store = self.store.scope(state: \.tabbar, action: \.tabbar) { | |
NavigationView { | |
TabBarView(store: store) | |
} | |
} | |
} | |
} | |
} | |
.onAppear { | |
store.send(.onAppear) | |
} | |
} | |
} | |
#if DEBUG | |
//struct StartupView_Previews: PreviewProvider { | |
// static var previews: some View { | |
// StartupView(store: StoreOf<Startup>( | |
// initialState: Startup.State() | |
// ){ | |
// Startup() | |
// }) | |
// } | |
//} | |
#endif | |
import AVFoundation | |
import Contacts | |
import CoreLocation | |
import Foundation | |
import UserNotifications | |
@DependencyClient | |
public struct DevicePermissions { | |
enum PermissionError: Error { | |
case cameraAccessDenied | |
case microphoneAccessDenied | |
case contactAccessDenied | |
case locationAccessDenied | |
case notificationAccessDenied | |
} | |
public var requestCameraPermissions: @Sendable () async throws -> Bool | |
public var requestMicrophonePermissions: @Sendable () async throws -> Bool | |
public var requestContactPermissions: @Sendable () async throws -> Bool | |
public var requestLocationPermissions: @Sendable () async throws -> Bool | |
public var requestNotificationPermissions: @Sendable () async throws -> Bool | |
} | |
extension DevicePermissions { | |
public static var live: DevicePermissions = .init( | |
requestCameraPermissions: { | |
let status = AVCaptureDevice.authorizationStatus(for: .video) | |
// Determine if the user previously authorized camera access. | |
var isAuthorized = status == .authorized | |
// If the system hasn't determined the user's authorization status, | |
// explicitly prompt them for approval. | |
if status == .notDetermined { | |
isAuthorized = await AVCaptureDevice.requestAccess(for: .video) | |
} | |
return isAuthorized | |
}, | |
requestMicrophonePermissions: { | |
try await withCheckedThrowingContinuation { continuation in | |
AVCaptureDevice.requestAccess(for: .audio) { granted in | |
if granted { | |
continuation.resume(returning: true) | |
} else { | |
continuation.resume(throwing: PermissionError.microphoneAccessDenied) | |
} | |
} | |
} | |
}, | |
requestContactPermissions: { | |
try await withCheckedThrowingContinuation { continuation in | |
let store = CNContactStore() | |
store.requestAccess(for: .contacts) { granted, error in | |
if let error = error { | |
continuation.resume(throwing: error) | |
} else if granted { | |
continuation.resume(returning: true) | |
} else { | |
continuation.resume(throwing: PermissionError.contactAccessDenied) | |
} | |
} | |
} | |
}, | |
requestLocationPermissions: { | |
return true | |
}, | |
requestNotificationPermissions: { | |
try await withCheckedThrowingContinuation { continuation in | |
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in | |
if let error = error { | |
continuation.resume(throwing: error) | |
} else if granted { | |
continuation.resume(returning: true) | |
} else { | |
continuation.resume(throwing: PermissionError.notificationAccessDenied) | |
} | |
} | |
} | |
} | |
) | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment