From 3e0d5d191388a1404668df244e78928465fc0704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=80=ED=98=81?= Date: Tue, 18 Feb 2025 00:17:32 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[#63]=20Firebase=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Core, RemoteConfig 패키지 추가 - AppDelegete 추가 및 Firebase 초기화 --- Soomsil-USaint.xcodeproj/project.pbxproj | 25 ++++ .../xcshareddata/swiftpm/Package.resolved | 119 +++++++++++++++++- .../Application/Rusaint_iOSApp.swift | 10 ++ Soomsil-USaint/GoogleService-Info.plist | 30 +++++ 4 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 Soomsil-USaint/GoogleService-Info.plist diff --git a/Soomsil-USaint.xcodeproj/project.pbxproj b/Soomsil-USaint.xcodeproj/project.pbxproj index f94304d..6aac650 100644 --- a/Soomsil-USaint.xcodeproj/project.pbxproj +++ b/Soomsil-USaint.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 02C46D912D0F45B200A3E717 /* RxRelay in Frameworks */ = {isa = PBXBuildFile; productRef = 02C46D902D0F45B200A3E717 /* RxRelay */; }; 7B75DB312D3A374B00FF2FBE /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 7B75DB302D3A374B00FF2FBE /* ComposableArchitecture */; }; 7BA4D7D32D17FAE700CF6689 /* Rusaint in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA4D7D22D17FAE700CF6689 /* Rusaint */; }; + B96F06662D63882B0047D33D /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = B96F06652D63882B0047D33D /* FirebaseCore */; }; + B96F06682D63882B0047D33D /* FirebaseRemoteConfig in Frameworks */ = {isa = PBXBuildFile; productRef = B96F06672D63882B0047D33D /* FirebaseRemoteConfig */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -54,7 +56,9 @@ 02C46CF52D0F2E9300A3E717 /* YDS-SwiftUI in Frameworks */, 02C46D8F2D0F45B200A3E717 /* RxCocoa in Frameworks */, 7B75DB312D3A374B00FF2FBE /* ComposableArchitecture in Frameworks */, + B96F06662D63882B0047D33D /* FirebaseCore in Frameworks */, 02C46D8D2D0F45B200A3E717 /* RxBlocking in Frameworks */, + B96F06682D63882B0047D33D /* FirebaseRemoteConfig in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -105,6 +109,8 @@ 02C46D902D0F45B200A3E717 /* RxRelay */, 7BA4D7D22D17FAE700CF6689 /* Rusaint */, 7B75DB302D3A374B00FF2FBE /* ComposableArchitecture */, + B96F06652D63882B0047D33D /* FirebaseCore */, + B96F06672D63882B0047D33D /* FirebaseRemoteConfig */, ); productName = "Rusaint-iOS"; productReference = 02C46CBE2D0F138700A3E717 /* Soomsil-USaint.app */; @@ -140,6 +146,7 @@ 02C46D8B2D0F45B200A3E717 /* XCRemoteSwiftPackageReference "RxSwift" */, 7BA4D7D12D17FAE700CF6689 /* XCRemoteSwiftPackageReference "rusaint-ios" */, 7B75DB2F2D3A374B00FF2FBE /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, + B96F06642D63882B0047D33D /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, ); preferredProjectObjectVersion = 77; productRefGroup = 02C46CBF2D0F138700A3E717 /* Products */; @@ -435,6 +442,14 @@ minimumVersion = 0.8.2; }; }; + B96F06642D63882B0047D33D /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/firebase/firebase-ios-sdk"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 11.8.1; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -478,6 +493,16 @@ package = 7BA4D7D12D17FAE700CF6689 /* XCRemoteSwiftPackageReference "rusaint-ios" */; productName = Rusaint; }; + B96F06652D63882B0047D33D /* FirebaseCore */ = { + isa = XCSwiftPackageProductDependency; + package = B96F06642D63882B0047D33D /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseCore; + }; + B96F06672D63882B0047D33D /* FirebaseRemoteConfig */ = { + isa = XCSwiftPackageProductDependency; + package = B96F06642D63882B0047D33D /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseRemoteConfig; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 02C46CB62D0F138700A3E717 /* Project object */; diff --git a/Soomsil-USaint.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Soomsil-USaint.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 71d363d..460f31a 100644 --- a/Soomsil-USaint.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Soomsil-USaint.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,24 @@ { - "originHash" : "3fad13c882a08211597ca586b50a9eb013bc8775f1ce7024a83dc890cfa545e1", + "originHash" : "2cd7be355296c8d8b7324fc9365a54f1715e432052a886a0c719e2356aa2c54f", "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "194a6706acbd25e4ef639bcaddea16e8758a3e27", + "version" : "1.2024011602.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, { "identity" : "combine-schedulers", "kind" : "remoteSourceControl", @@ -10,6 +28,69 @@ "version" : "1.0.3" } }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "6318278e8e64d21f0fdcc69004395e4d34048aaf", + "version" : "11.8.1" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "be0881ff728eca210ccb628092af400c086abda3", + "version" : "11.7.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", + "version" : "8.0.2" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "f56d8fc3162de9a498377c7b6cea43431f4f5083", + "version" : "1.65.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "3cdb78efb79b4a5383c3911488d8025bfc545b5e", + "version" : "4.3.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", + "version" : "100.0.0" + } + }, { "identity" : "keychainaccess", "kind" : "remoteSourceControl", @@ -19,6 +100,24 @@ "revision" : "e0c7eebc5a4465a3c4680764f26b7a61f567cdaf" } }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, { "identity" : "panmodal", "kind" : "remoteSourceControl", @@ -37,6 +136,15 @@ "version" : "3.7.2" } }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, { "identity" : "rusaint-ios", "kind" : "remoteSourceControl", @@ -154,6 +262,15 @@ "version" : "1.4.1" } }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "ebc7251dd5b37f627c93698e4374084d98409633", + "version" : "1.28.2" + } + }, { "identity" : "swift-sharing", "kind" : "remoteSourceControl", diff --git a/Soomsil-USaint/Application/Rusaint_iOSApp.swift b/Soomsil-USaint/Application/Rusaint_iOSApp.swift index 306e6d2..594b4bb 100644 --- a/Soomsil-USaint/Application/Rusaint_iOSApp.swift +++ b/Soomsil-USaint/Application/Rusaint_iOSApp.swift @@ -9,10 +9,20 @@ import SwiftUI import BackgroundTasks import ComposableArchitecture +import FirebaseCore import Rusaint +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + FirebaseApp.configure() + return true + } +} + @main struct Rusaint_iOSApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate + @Environment(\.scenePhase) var scenePhase let store = Store(initialState: AppReducer.State()) { AppReducer() } diff --git a/Soomsil-USaint/GoogleService-Info.plist b/Soomsil-USaint/GoogleService-Info.plist new file mode 100644 index 0000000..2bf399e --- /dev/null +++ b/Soomsil-USaint/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyDpHLpOch4gBX2fqS313wTrQH2IUzHmlg0 + GCM_SENDER_ID + 52701294141 + PLIST_VERSION + 1 + BUNDLE_ID + com.yourssu.SaintKit + PROJECT_ID + soomsil-usaint + STORAGE_BUCKET + soomsil-usaint.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:52701294141:ios:e6a52f5369e1afd0814454 + + \ No newline at end of file From efcbb7175e7d3e68acf48c1b9357d695bdf8cdb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=80=ED=98=81?= Date: Tue, 18 Feb 2025 00:55:15 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[#63]=20RemoteConfigClient=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getMinimumVersion 내에 active 및 fetch 구현 --- Soomsil-USaint/Application/AppReducer.swift | 16 +++++++ Soomsil-USaint/Application/AppView.swift | 2 +- .../Client/Firebase/RemoteConfigClient.swift | 46 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 Soomsil-USaint/Data/Client/Firebase/RemoteConfigClient.swift diff --git a/Soomsil-USaint/Application/AppReducer.swift b/Soomsil-USaint/Application/AppReducer.swift index 1999747..3a51558 100644 --- a/Soomsil-USaint/Application/AppReducer.swift +++ b/Soomsil-USaint/Application/AppReducer.swift @@ -23,6 +23,8 @@ struct AppReducer { } enum Action { + case checkMinimumVersion + case checkMinimumVersionResponse(Result) case initialize case initResponse(Result<(StudentInfo, TotalReportCard), Error>) case backgroundTask @@ -31,12 +33,26 @@ struct AppReducer { } @Dependency(\.localNotificationClient) var localNotificationClient + @Dependency(\.remoteConfigClient) var remoteConfigClient @Dependency(\.gradeClient) var gradeClient @Dependency(\.studentClient) var studentClient var body: some Reducer { Reduce { state, action in switch action { + case .checkMinimumVersion: + return .run { send in + await send(.checkMinimumVersionResponse(Result { + return try await remoteConfigClient.getMinimumVersion() + })) + } + case .checkMinimumVersionResponse(.success(let minimumVersion)): + debugPrint("MinimumVersion at AppReducer - \(minimumVersion)") + return .send(.initialize) + case .checkMinimumVersionResponse(.failure(let error)): + debugPrint("Error at AppReducer - \(error)") + // TODO: 에러 처리 + return .none case .initialize: return .run { send in await send(.initResponse(Result { diff --git a/Soomsil-USaint/Application/AppView.swift b/Soomsil-USaint/Application/AppView.swift index 1aa55e9..f0db4c0 100644 --- a/Soomsil-USaint/Application/AppView.swift +++ b/Soomsil-USaint/Application/AppView.swift @@ -17,7 +17,7 @@ struct AppView: View { case .initial: SplashView() .onAppear { - store.send(.initialize) + store.send(.checkMinimumVersion) } case .loggedOut: if let store = store.scope(state: \.loggedOut, action: \.login) { diff --git a/Soomsil-USaint/Data/Client/Firebase/RemoteConfigClient.swift b/Soomsil-USaint/Data/Client/Firebase/RemoteConfigClient.swift new file mode 100644 index 0000000..3a7e5ef --- /dev/null +++ b/Soomsil-USaint/Data/Client/Firebase/RemoteConfigClient.swift @@ -0,0 +1,46 @@ +// +// RemoteConfigClient.swift +// Soomsil-USaint +// +// Created by 정지혁 on 2/18/25. +// + +import Foundation + +import ComposableArchitecture +import FirebaseRemoteConfig + +@DependencyClient +struct RemoteConfigClient { + private static let minimumVersionkey: String = "min_version_ios" + + var getMinimumVersion: @Sendable () async throws -> String +} + +extension DependencyValues { + var remoteConfigClient: RemoteConfigClient { + get { self[RemoteConfigClient.self] } + set { self[RemoteConfigClient.self] = newValue } + } +} + +extension RemoteConfigClient: DependencyKey { + static let liveValue: RemoteConfigClient = Self( + getMinimumVersion: { + let remoteConfig = RemoteConfig.remoteConfig() + try await remoteConfig.fetchAndActivate() + + let minimumVersion = remoteConfig[minimumVersionkey].stringValue + debugPrint(minimumVersion) + return minimumVersion + } + ) + + static let previewValue: RemoteConfigClient = Self( + getMinimumVersion: { + return "1.0.0" + } + ) + + static let testValue: RemoteConfigClient = previewValue +} From 0e70c04e72f0eeb2a864ea1aa85229c0ac25fcfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=80=ED=98=81?= Date: Tue, 18 Feb 2025 01:07:49 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[#63]=20=EC=97=90=EB=9F=AC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20PreviewValue=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Soomsil-USaint/Application/AppReducer.swift | 3 +-- Soomsil-USaint/Data/Client/Firebase/RemoteConfigClient.swift | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Soomsil-USaint/Application/AppReducer.swift b/Soomsil-USaint/Application/AppReducer.swift index 3a51558..0e631dd 100644 --- a/Soomsil-USaint/Application/AppReducer.swift +++ b/Soomsil-USaint/Application/AppReducer.swift @@ -51,8 +51,7 @@ struct AppReducer { return .send(.initialize) case .checkMinimumVersionResponse(.failure(let error)): debugPrint("Error at AppReducer - \(error)") - // TODO: 에러 처리 - return .none + return .send(.initialize) case .initialize: return .run { send in await send(.initResponse(Result { diff --git a/Soomsil-USaint/Data/Client/Firebase/RemoteConfigClient.swift b/Soomsil-USaint/Data/Client/Firebase/RemoteConfigClient.swift index 3a7e5ef..b2c8c14 100644 --- a/Soomsil-USaint/Data/Client/Firebase/RemoteConfigClient.swift +++ b/Soomsil-USaint/Data/Client/Firebase/RemoteConfigClient.swift @@ -38,7 +38,7 @@ extension RemoteConfigClient: DependencyKey { static let previewValue: RemoteConfigClient = Self( getMinimumVersion: { - return "1.0.0" + return "3.0.2" } ) From 5d326036f4ffc75b4d2bcd44059f343ac821b1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=80=ED=98=81?= Date: Thu, 27 Feb 2025 17:23:55 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[#63]=20SplashReducer=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 파일 및 기본 구조 추가 --- .../Feature/Splash/Core/SplashReducer.swift | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 Soomsil-USaint/Application/Feature/Splash/Core/SplashReducer.swift diff --git a/Soomsil-USaint/Application/Feature/Splash/Core/SplashReducer.swift b/Soomsil-USaint/Application/Feature/Splash/Core/SplashReducer.swift new file mode 100644 index 0000000..40c29d2 --- /dev/null +++ b/Soomsil-USaint/Application/Feature/Splash/Core/SplashReducer.swift @@ -0,0 +1,38 @@ +// +// SplashReducer.swift +// Soomsil-USaint +// +// Created by 정지혁 on 2/27/25. +// + +import Foundation + +import ComposableArchitecture + +@Reducer +struct SplashReducer { + @ObservableState + struct State { + @Presents var alert: AlertState? + } + + enum Action { + case alert(PresentationAction) + case checkMinimumVersion + case checkMinimumVersionResponse(Result) + case initialize + case initResponse(Result) + + enum Alert: Equatable { + case moveAppStoreTapped + } + } + + @Dependency(\.remoteConfigClient) var remoteConfigClient + + var body: some Reducer { + Reduce { state, action in + return .none + } + } +} From 276ba467c3c7728f1c575100d1553d810786a1c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=80=ED=98=81?= Date: Tue, 4 Mar 2025 22:48:48 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20SplashView,=20Reducer=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppReducer 내 ifCaseLet 추가 - 최소 버전 비교 로직 구현 - previewValue 수정 --- Soomsil-USaint/Application/AppReducer.swift | 53 ++++---------- Soomsil-USaint/Application/AppView.swift | 7 +- .../Feature/Splash/Core/SplashReducer.swift | 72 ++++++++++++++++++- .../Feature/Splash/View/SplashView.swift | 22 +++++- .../Client/Firebase/RemoteConfigClient.swift | 3 +- 5 files changed, 105 insertions(+), 52 deletions(-) diff --git a/Soomsil-USaint/Application/AppReducer.swift b/Soomsil-USaint/Application/AppReducer.swift index 0e631dd..21fef6d 100644 --- a/Soomsil-USaint/Application/AppReducer.swift +++ b/Soomsil-USaint/Application/AppReducer.swift @@ -13,71 +13,39 @@ import ComposableArchitecture struct AppReducer { @ObservableState enum State { - case initial + case initial(SplashReducer.State) case loggedOut(LoginReducer.State) case loggedIn(HomeReducer.State) init() { - self = .initial + self = .initial(SplashReducer.State()) } } enum Action { - case checkMinimumVersion - case checkMinimumVersionResponse(Result) - case initialize - case initResponse(Result<(StudentInfo, TotalReportCard), Error>) case backgroundTask + case splash(SplashReducer.Action) case login(LoginReducer.Action) case home(HomeReducer.Action) } @Dependency(\.localNotificationClient) var localNotificationClient - @Dependency(\.remoteConfigClient) var remoteConfigClient - @Dependency(\.gradeClient) var gradeClient - @Dependency(\.studentClient) var studentClient var body: some Reducer { Reduce { state, action in switch action { - case .checkMinimumVersion: - return .run { send in - await send(.checkMinimumVersionResponse(Result { - return try await remoteConfigClient.getMinimumVersion() - })) - } - case .checkMinimumVersionResponse(.success(let minimumVersion)): - debugPrint("MinimumVersion at AppReducer - \(minimumVersion)") - return .send(.initialize) - case .checkMinimumVersionResponse(.failure(let error)): - debugPrint("Error at AppReducer - \(error)") - return .send(.initialize) - case .initialize: - return .run { send in - await send(.initResponse(Result { - let _ = try await studentClient.getSaintInfo() - try await gradeClient.deleteTotalReportCard() - let rusaintReport = try await gradeClient.fetchTotalReportCard() - try await gradeClient.updateTotalReportCard(rusaintReport) - - let info = try await studentClient.getStudentInfo() - let report = try await gradeClient.getTotalReportCard() - return (info, report) - })) - } - case .initResponse(.success(let (info, report))): - state = .loggedIn(HomeReducer.State(studentInfo: info, totalReportCard: report)) - return .none - case .initResponse(.failure(let error)): - debugPrint(error) - state = .loggedOut(LoginReducer.State()) - return .none case .backgroundTask: debugPrint("AppReducer: backgroundTask") return .run { send in @Shared(.appStorage("isFirst")) var isFirst = true try await localNotificationClient.setLecturePushNotification("\(isFirst)") } + case .splash(.initResponse(.success(let (studentInfo, totalReportCard)))): + state = .loggedIn(HomeReducer.State(studentInfo: studentInfo, totalReportCard: totalReportCard)) + return .none + case .splash(.initResponse(.failure)): + state = .loggedOut(LoginReducer.State()) + return .none case .login(.loginResponse(.success(let (info, report)))): state = .loggedIn(HomeReducer.State(studentInfo: info, totalReportCard: report)) return .none @@ -88,6 +56,9 @@ struct AppReducer { return .none } } + .ifCaseLet(\.initial, action: \.splash) { + SplashReducer() + } .ifCaseLet(\.loggedOut, action: \.login) { LoginReducer() } diff --git a/Soomsil-USaint/Application/AppView.swift b/Soomsil-USaint/Application/AppView.swift index f0db4c0..7e9545a 100644 --- a/Soomsil-USaint/Application/AppView.swift +++ b/Soomsil-USaint/Application/AppView.swift @@ -15,10 +15,9 @@ struct AppView: View { var body: some View { switch store.state { case .initial: - SplashView() - .onAppear { - store.send(.checkMinimumVersion) - } + if let store = store.scope(state: \.initial, action: \.splash) { + SplashView(store: store) + } case .loggedOut: if let store = store.scope(state: \.loggedOut, action: \.login) { LoginView(store: store) diff --git a/Soomsil-USaint/Application/Feature/Splash/Core/SplashReducer.swift b/Soomsil-USaint/Application/Feature/Splash/Core/SplashReducer.swift index 40c29d2..088e4f2 100644 --- a/Soomsil-USaint/Application/Feature/Splash/Core/SplashReducer.swift +++ b/Soomsil-USaint/Application/Feature/Splash/Core/SplashReducer.swift @@ -21,18 +21,86 @@ struct SplashReducer { case checkMinimumVersion case checkMinimumVersionResponse(Result) case initialize - case initResponse(Result) + case initResponse(Result<(StudentInfo, TotalReportCard), Error>) enum Alert: Equatable { + case confirmTapped case moveAppStoreTapped } } @Dependency(\.remoteConfigClient) var remoteConfigClient + @Dependency(\.gradeClient) var gradeClient + @Dependency(\.studentClient) var studentClient + @Dependency(\.openURL) var openURL var body: some Reducer { Reduce { state, action in - return .none + switch action { + case .alert(.presented(.confirmTapped)): + return .none + case .alert(.presented(.moveAppStoreTapped)): + return .run { _ in + await openURL(URL(string: "itms-apps://itunes.apple.com/app/id1601044486")!) + } + case .checkMinimumVersion: + return .run { send in + await send(.checkMinimumVersionResponse(Result { + return try await remoteConfigClient.getMinimumVersion() + })) + } + case .checkMinimumVersionResponse(.success(let minimumVersion)): + if checkMinimumVersion(minimum: minimumVersion) { + return .send(.initialize) + } else { + state.alert = AlertState( + title: { TextState("앱 업데이트가 있어요") }, + actions: { + ButtonState(action: .send(.moveAppStoreTapped)) { + TextState("스토어로 이동하기") + } + }, + message: { + TextState("원활한 서비스 이용을 위해\n업데이트가 필요해요") + } + ) + return .none + } + case .checkMinimumVersionResponse(.failure(let error)): + debugPrint(error.localizedDescription) + state.alert = AlertState( + title: { TextState("네트워크 에러") }, + actions: { + ButtonState(action: .send(.confirmTapped)) { + TextState("확인") + } + }, + message: { + TextState(error.localizedDescription) + } + ) + return .none + case .initialize: + return .run { send in + await send(.initResponse(Result { + let info = try await studentClient.getStudentInfo() + let card = try await gradeClient.getTotalReportCard() + return (info, card) + })) + } + default: + return .none + } } + .ifLet(\.$alert, action: \.alert) + } + + func checkMinimumVersion(minimum minimumVersion: String) -> Bool { + let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0" + + let current = currentVersion.split(separator: ".").map { Int($0) ?? 0 } + let minimum = minimumVersion.split(separator: ".").map { Int($0) ?? 0 } + + return (current[0], current[1], current[2]) >= (minimum[0], minimum[1], minimum[2]) } } diff --git a/Soomsil-USaint/Application/Feature/Splash/View/SplashView.swift b/Soomsil-USaint/Application/Feature/Splash/View/SplashView.swift index 71c43d4..3903be0 100644 --- a/Soomsil-USaint/Application/Feature/Splash/View/SplashView.swift +++ b/Soomsil-USaint/Application/Feature/Splash/View/SplashView.swift @@ -7,10 +7,26 @@ import SwiftUI +import ComposableArchitecture + struct SplashView: View { + @Perception.Bindable var store: StoreOf + var body: some View { - Image("splash") - .resizable() - .aspectRatio(contentMode: .fill) + WithPerceptionTracking { + Image("splash") + .resizable() + .aspectRatio(contentMode: .fill) + .alert($store.scope(state: \.alert, action: \.alert)) + } + .onAppear { + store.send(.checkMinimumVersion) + } } } + +#Preview { + SplashView(store: Store(initialState: SplashReducer.State()) { + SplashReducer() + }) +} diff --git a/Soomsil-USaint/Data/Client/Firebase/RemoteConfigClient.swift b/Soomsil-USaint/Data/Client/Firebase/RemoteConfigClient.swift index b2c8c14..6d1eb10 100644 --- a/Soomsil-USaint/Data/Client/Firebase/RemoteConfigClient.swift +++ b/Soomsil-USaint/Data/Client/Firebase/RemoteConfigClient.swift @@ -31,14 +31,13 @@ extension RemoteConfigClient: DependencyKey { try await remoteConfig.fetchAndActivate() let minimumVersion = remoteConfig[minimumVersionkey].stringValue - debugPrint(minimumVersion) return minimumVersion } ) static let previewValue: RemoteConfigClient = Self( getMinimumVersion: { - return "3.0.2" + return "3.0.3" } )