Skip to content

Commit d92deed

Browse files
authored
Merge pull request #264 from YAPP-Github/BOOK-435-feature/#263
feat: RemoteConfig를 통한 강제 업데이트 처리 분기
2 parents e0af5e0 + f004f05 commit d92deed

11 files changed

Lines changed: 185 additions & 21 deletions

File tree

src/Projects/BKData/Project.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ let project = Project.project(
1717
.domain(),
1818
.external(dependency: .KakaoSDKCommon),
1919
.external(dependency: .KakaoSDKAuth),
20-
.external(dependency: .KakaoSDKUser)
20+
.external(dependency: .KakaoSDKUser),
21+
.external(dependency: .FirebaseRemoteConfig)
2122
]
2223
),
2324
Target.target(

src/Projects/BKData/Sources/DataAssembly.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import BKCore
44
import BKDomain
55
import Foundation
6+
import FirebaseRemoteConfig
67

78
public struct DataAssembly: Assembly {
89
public init() {}
@@ -129,6 +130,19 @@ public struct DataAssembly: Assembly {
129130
@Autowired var networkProvider: NetworkProvider
130131
return DefaultAppStoreRepository(networkProvider: networkProvider)
131132
}
133+
134+
container.register(
135+
type: RemoteConfigRepository.self,
136+
scope: .singleton) { _ in
137+
let remoteConfig = RemoteConfig.remoteConfig()
138+
let settings = RemoteConfigSettings()
139+
#if DEBUG
140+
settings.minimumFetchInterval = 0
141+
#endif
142+
remoteConfig.configSettings = settings
143+
return DefaultRemoteConfigRepository(remoteConfig: remoteConfig)
144+
}
145+
132146

133147
container.register(
134148
type: NotificationRepository.self
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright © 2025 Booket. All rights reserved
2+
3+
import BKDomain
4+
import Combine
5+
import Foundation
6+
import FirebaseRemoteConfig
7+
8+
enum RemoteConfigKeys {
9+
static let latestVersion = "appleLatestVersion"
10+
static let minimumRequiredVersion = "appleMinimumVersion"
11+
}
12+
13+
public struct DefaultRemoteConfigRepository: RemoteConfigRepository {
14+
15+
private let remoteConfig: RemoteConfig
16+
17+
public init(remoteConfig: RemoteConfig) {
18+
self.remoteConfig = remoteConfig
19+
self.setDefaults()
20+
}
21+
22+
public func fetchRemoteAppVersions() -> AnyPublisher<RemoteAppVersion, Error> {
23+
return Future<RemoteAppVersion, Error> { promise in
24+
self.remoteConfig.fetchAndActivate { status, error in
25+
if let error = error {
26+
promise(.failure(error))
27+
return
28+
}
29+
30+
if status != .error {
31+
let latest = self.remoteConfig.configValue(
32+
forKey: RemoteConfigKeys.latestVersion
33+
).stringValue
34+
35+
let minimum = self.remoteConfig.configValue(
36+
forKey: RemoteConfigKeys.minimumRequiredVersion
37+
).stringValue
38+
39+
let versions = RemoteAppVersion(
40+
latestVersion: latest,
41+
minimumRequiredVersion: minimum
42+
)
43+
promise(.success(versions))
44+
} else {
45+
promise(.failure(URLError(.cannotParseResponse)))
46+
}
47+
}
48+
}
49+
.eraseToAnyPublisher()
50+
}
51+
52+
private func setDefaults() {
53+
let defaultValues: [String: NSObject] = [
54+
RemoteConfigKeys.latestVersion: "0.0.0" as NSObject,
55+
RemoteConfigKeys.minimumRequiredVersion: "0.0.0" as NSObject
56+
]
57+
self.remoteConfig.setDefaults(defaultValues)
58+
}
59+
}

src/Projects/BKDomain/Sources/DomainAssembly.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ public struct DomainAssembly: Assembly {
7070
return DefaultAppVersionUseCase(repository: repository)
7171
}
7272

73+
container.register(
74+
type: FetchRemoteAppVersionUseCase.self
75+
) { _ in
76+
@Autowired var repository: RemoteConfigRepository
77+
return DefaultFetchRemoteAppVersionUseCase(repository: repository)
78+
}
79+
7380
container.register(
7481
type: SearchBookUseCase.self
7582
) { _ in
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright © 2025 Booket. All rights reserved
2+
3+
import Foundation
4+
5+
public struct RemoteAppVersion {
6+
public let latestVersion: String
7+
public let minimumRequiredVersion: String
8+
9+
public init(latestVersion: String, minimumRequiredVersion: String) {
10+
self.latestVersion = latestVersion
11+
self.minimumRequiredVersion = minimumRequiredVersion
12+
}
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright © 2025 Booket. All rights reserved
2+
3+
import Combine
4+
import Foundation
5+
6+
public protocol RemoteConfigRepository {
7+
func fetchRemoteAppVersions() -> AnyPublisher<RemoteAppVersion, Error>
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright © 2025 Booket. All rights reserved
2+
3+
import Combine
4+
import Foundation
5+
6+
/// Remote Config에서 앱 버전 정보(최신, 최소)를 가져옵니다.
7+
public protocol FetchRemoteAppVersionUseCase {
8+
func execute() -> AnyPublisher<RemoteAppVersion, Error>
9+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright © 2025 Booket. All rights reserved
2+
3+
import Combine
4+
import Foundation
5+
6+
public struct DefaultFetchRemoteAppVersionUseCase: FetchRemoteAppVersionUseCase {
7+
8+
private let repository: RemoteConfigRepository
9+
10+
public init(repository: RemoteConfigRepository) {
11+
self.repository = repository
12+
}
13+
14+
public func execute() -> AnyPublisher<RemoteAppVersion, Error> {
15+
return repository.fetchRemoteAppVersions()
16+
}
17+
}

src/Projects/BKPresentation/Sources/AppCoordinator.swift

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public final class AppCoordinator: Coordinator, AuthenticationRequiredNotifying,
1919
private let onboardingCheckUseCase: OnboardingCheckUseCase
2020
private let markOnboardingSeenUseCase: MarkOnboardingSeenUseCase
2121
private let appVersionUseCase: AppVersionUseCase
22+
private let fetchRemoteAppVersionUseCase: FetchRemoteAppVersionUseCase
2223
private let syncFCMTokenUseCase: SyncFCMTokenUseCase
2324
private var cancellable: Set<AnyCancellable> = []
2425

@@ -28,13 +29,15 @@ public final class AppCoordinator: Coordinator, AuthenticationRequiredNotifying,
2829
onboardingCheckUseCase: OnboardingCheckUseCase,
2930
markOnboardingSeenUseCase: MarkOnboardingSeenUseCase,
3031
appVersionUseCase: AppVersionUseCase,
32+
fetchRemoteAppVersionUseCase: FetchRemoteAppVersionUseCase,
3133
syncFCMTokenUseCase: SyncFCMTokenUseCase
3234
) {
3335
self.navigationController = navigationController
3436
self.authStateUseCase = authStateUseCase
3537
self.onboardingCheckUseCase = onboardingCheckUseCase
3638
self.markOnboardingSeenUseCase = markOnboardingSeenUseCase
3739
self.appVersionUseCase = appVersionUseCase
40+
self.fetchRemoteAppVersionUseCase = fetchRemoteAppVersionUseCase
3841
self.syncFCMTokenUseCase = syncFCMTokenUseCase
3942
}
4043

@@ -67,25 +70,34 @@ public final class AppCoordinator: Coordinator, AuthenticationRequiredNotifying,
6770
private func checkAppUpdate() {
6871
Publishers.Zip(
6972
appVersionUseCase.execute().setFailureType(to: Error.self),
70-
appVersionUseCase.executeRecentVersion()
73+
fetchRemoteAppVersionUseCase.execute()
7174
)
7275
.receive(on: DispatchQueue.main)
7376
.sink(receiveCompletion: { [weak self] completion in
7477
if case .failure = completion {
7578
self?.proceedWithAppFlow()
7679
}
77-
}, receiveValue: { [weak self] currentVersionString, latestVersionString in
80+
}, receiveValue: { [weak self] currentVersionString, remoteVersions in
7881
guard let self = self,
7982
let currentVersion = Version(currentVersionString),
80-
let latestVersion = Version(latestVersionString)
83+
let minimumVersion = Version(remoteVersions.minimumRequiredVersion),
84+
let latestVersion = Version(remoteVersions.latestVersion)
8185
else {
8286
self?.proceedWithAppFlow()
8387
return
8488
}
8589

86-
if currentVersion.isMajorOrMinorUpdateRequired(from: latestVersion) {
87-
self.presentUpdateSheet()
88-
} else {
90+
Log.debug("currentVersionString: \(currentVersionString)", logger: AppLogger.ui)
91+
Log.debug("minimumRequiredVersion: \(remoteVersions.minimumRequiredVersion)", logger: AppLogger.ui)
92+
Log.debug("latestVersion: \(remoteVersions.latestVersion)", logger: AppLogger.ui)
93+
94+
if currentVersion < minimumVersion {
95+
self.presentUpdateSheet() // 강업
96+
}
97+
else if currentVersion < latestVersion {
98+
self.presentUpdateSheet(isForced: false) // 권장
99+
}
100+
else {
89101
self.proceedWithAppFlow()
90102
}
91103
})
@@ -203,21 +215,41 @@ public final class AppCoordinator: Coordinator, AuthenticationRequiredNotifying,
203215
.store(in: &cancellable)
204216
}
205217

206-
private func presentUpdateSheet() {
207-
let dialog = BKDialog(
208-
title: "최신 버전이 출시되었습니다",
209-
subtitle: "최적의 사용 환경을 위해 업데이트해주세요.",
210-
config: .init(
211-
leftButtonTitle: "업데이트 하기",
212-
leftButtonAction: AppStoreLinker.openAppStore
218+
private func presentUpdateSheet(isForced: Bool = true) {
219+
var dialog: BKDialog?
220+
221+
if isForced {
222+
dialog = BKDialog(
223+
title: "최신 버전이 출시되었습니다",
224+
subtitle: "최적의 사용 환경을 위해 업데이트해주세요.",
225+
config: .init(
226+
leftButtonTitle: "업데이트 하기",
227+
leftButtonAction: AppStoreLinker.openAppStore
228+
)
213229
)
214-
)
215-
230+
} else {
231+
// TODO(dyk) : 디자인 파트와 논의 후 subtitle 수정하기
232+
dialog = BKDialog(
233+
title: "최신 버전이 출시되었습니다",
234+
subtitle: "최적의 사용 환경을 위해 업데이트해주세요.",
235+
config: .init(
236+
leftButtonTitle: "업데이트 하기",
237+
leftButtonAction: AppStoreLinker.openAppStore,
238+
rightButtonTitle: "나중에 하기",
239+
rightButtonAction: { [weak self] in
240+
guard let self else { return }
241+
self.navigationController.dismiss(animated: true) {
242+
self.proceedWithAppFlow()
243+
}
244+
}
245+
)
246+
)
247+
}
248+
249+
guard let dialog else { return }
216250
let dialogViewController = BKDialogViewController(dialog: dialog)
217251
dialogViewController.isModalInPresentation = true
218-
DispatchQueue.main.async {
219-
self.navigationController.present(dialogViewController, animated: true)
220-
}
252+
navigationController.present(dialogViewController, animated: true)
221253
}
222254

223255
private func requestNotificationPermissionIfNeeded() {

src/Projects/Booket/Project.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ let debugAppTarget = Target.target(
3131
.external(dependency: .FirebaseCore),
3232
.external(dependency: .FirebaseCrashlytics),
3333
.external(dependency: .FirebaseAnalytics),
34-
.external(dependency: .FirebaseMessaging)
34+
.external(dependency: .FirebaseMessaging),
35+
.external(dependency: .FirebaseRemoteConfig)
3536
],
3637
settings: .settings(
3738
base: [
@@ -76,7 +77,8 @@ let releaseAppTarget = Target.target(
7677
.external(dependency: .FirebaseCore),
7778
.external(dependency: .FirebaseCrashlytics),
7879
.external(dependency: .FirebaseAnalytics),
79-
.external(dependency: .FirebaseMessaging)
80+
.external(dependency: .FirebaseMessaging),
81+
.external(dependency: .FirebaseRemoteConfig)
8082
],
8183
settings: .settings(
8284
base: [

0 commit comments

Comments
 (0)