add notice update
This commit is contained in:
parent
84a8706b0a
commit
dd61970357
@ -1766,6 +1766,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Обновить приложение" : {
|
||||
|
||||
},
|
||||
"Обратная связь" : {
|
||||
"comment" : "feedback: navigation title",
|
||||
|
||||
164
yobble/Services/AppUpdateChecker.swift
Normal file
164
yobble/Services/AppUpdateChecker.swift
Normal file
@ -0,0 +1,164 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct AppUpdateNotice: Identifiable {
|
||||
enum Kind {
|
||||
case force
|
||||
case soft
|
||||
}
|
||||
|
||||
let id = UUID()
|
||||
let kind: Kind
|
||||
let title: String
|
||||
let message: String
|
||||
let appStoreURL: URL
|
||||
}
|
||||
|
||||
final class AppUpdateChecker: ObservableObject {
|
||||
@Published private(set) var softUpdateNotice: AppUpdateNotice?
|
||||
@Published private(set) var forceUpdateNotice: AppUpdateNotice?
|
||||
|
||||
private let session: URLSession
|
||||
private var didStartCheck = false
|
||||
|
||||
init(session: URLSession = .shared) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
func checkForUpdatesIfNeeded() {
|
||||
guard !didStartCheck else { return }
|
||||
didStartCheck = true
|
||||
|
||||
Task { await fetchRemoteConfig() }
|
||||
}
|
||||
|
||||
func dismissSoftUpdateIfNeeded() {
|
||||
softUpdateNotice = nil
|
||||
}
|
||||
|
||||
func openAppStore() {
|
||||
guard let url = forceUpdateNotice?.appStoreURL ?? softUpdateNotice?.appStoreURL else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchRemoteConfig() async {
|
||||
guard
|
||||
let buildType = AppConfig.APP_BUILD.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
|
||||
let url = URL(string: "https://static.yobble.org/config/ios/dev2-\(buildType).json")
|
||||
else {
|
||||
log("Unable to build remote config URL")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let (data, response) = try await session.data(from: url)
|
||||
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
|
||||
log("Unexpected response when fetching remote config")
|
||||
return
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
let remoteConfig = try decoder.decode(RemoteBuildConfiguration.self, from: data)
|
||||
await MainActor.run {
|
||||
self.apply(remoteConfig)
|
||||
}
|
||||
} catch {
|
||||
log("Failed to fetch remote config: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func apply(_ config: RemoteBuildConfiguration) {
|
||||
guard let buildNumber = currentBuildNumber() else {
|
||||
log("Unable to read current build number")
|
||||
return
|
||||
}
|
||||
|
||||
forceUpdateNotice = nil
|
||||
softUpdateNotice = nil
|
||||
|
||||
guard let appStoreURL = config.appStoreURL else {
|
||||
log("Config missing App Store URL")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
print("buildNumber", buildNumber)
|
||||
print("config", config.notSupportedBuild, config.minSupportedBuild, config.recommendedBuild)
|
||||
let requiresDoUpdate = buildNumber <= config.notSupportedBuild
|
||||
if requiresDoUpdate, let info = config.forceUpdate {
|
||||
forceUpdateNotice = AppUpdateNotice(kind: .force, title: info.title, message: info.message, appStoreURL: appStoreURL)
|
||||
return
|
||||
}
|
||||
|
||||
let requiresForcedUpdate = buildNumber < config.minSupportedBuild
|
||||
if requiresForcedUpdate, let info = config.forceUpdate {
|
||||
forceUpdateNotice = AppUpdateNotice(kind: .force, title: info.title, message: info.message, appStoreURL: appStoreURL)
|
||||
return
|
||||
}
|
||||
|
||||
let needsSoftUpdate = buildNumber < config.recommendedBuild
|
||||
if needsSoftUpdate, let info = config.softUpdate {
|
||||
softUpdateNotice = AppUpdateNotice(kind: .soft, title: info.title, message: info.message, appStoreURL: appStoreURL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func currentBuildNumber() -> Int? {
|
||||
guard let rawValue = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
|
||||
return nil
|
||||
}
|
||||
return Int(rawValue)
|
||||
}
|
||||
|
||||
private func log(_ message: String) {
|
||||
if AppConfig.DEBUG {
|
||||
print("[AppUpdateChecker]", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct RemoteBuildConfiguration: Decodable {
|
||||
struct UpdateInfo: Decodable {
|
||||
let title: String
|
||||
let message: String
|
||||
}
|
||||
|
||||
let schemaVersion: Int
|
||||
let notSupportedBuild: Int
|
||||
let minSupportedBuild: Int
|
||||
let recommendedBuild: Int
|
||||
let forceUpdate: UpdateInfo?
|
||||
let softUpdate: UpdateInfo?
|
||||
let appStoreURL: URL?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case schemaVersion = "schema_version"
|
||||
case notSupportedBuild = "not_supported_build"
|
||||
case minSupportedBuild = "min_supported_build"
|
||||
case recommendedBuild = "recommended_build"
|
||||
case forceUpdate = "force_update"
|
||||
case softUpdate = "soft_update"
|
||||
case appStoreURL = "appstore_url"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
schemaVersion = try container.decodeIfPresent(Int.self, forKey: .schemaVersion) ?? 1
|
||||
notSupportedBuild = try container.decode(Int.self, forKey: .notSupportedBuild)
|
||||
minSupportedBuild = try container.decode(Int.self, forKey: .minSupportedBuild)
|
||||
recommendedBuild = try container.decode(Int.self, forKey: .recommendedBuild)
|
||||
forceUpdate = try container.decodeIfPresent(UpdateInfo.self, forKey: .forceUpdate)
|
||||
softUpdate = try container.decodeIfPresent(UpdateInfo.self, forKey: .softUpdate)
|
||||
if let urlString = try container.decodeIfPresent(String.self, forKey: .appStoreURL) {
|
||||
appStoreURL = URL(string: urlString)
|
||||
} else {
|
||||
appStoreURL = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
50
yobble/Views/ForceUpdateView.swift
Normal file
50
yobble/Views/ForceUpdateView.swift
Normal file
@ -0,0 +1,50 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ForceUpdateView: View {
|
||||
let title: String
|
||||
let message: String
|
||||
let onUpdate: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black.opacity(0.6)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 16) {
|
||||
Text(title)
|
||||
.font(.title3.weight(.semibold))
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(message)
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Button(action: onUpdate) {
|
||||
Text(NSLocalizedString("Обновить приложение", comment: ""))
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.accentColor)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.background(.ultraThinMaterial)
|
||||
.cornerRadius(20)
|
||||
.padding(32)
|
||||
}
|
||||
.zIndex(2)
|
||||
.accessibilityElement(children: .contain)
|
||||
}
|
||||
}
|
||||
|
||||
struct ForceUpdateView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ForceUpdateView(title: "Требуется обновление",
|
||||
message: "Эта версия приложения устарела и больше не поддерживается.",
|
||||
onUpdate: {})
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AppConfig {
|
||||
static var DEBUG: Bool = true
|
||||
static var DEBUG: Bool = false
|
||||
//static let SERVICE = Bundle.main.bundleIdentifier ?? "default.service"
|
||||
static let PROTOCOL = "https"
|
||||
static let API_SERVER = "\(PROTOCOL)://api.yobble.org"
|
||||
|
||||
@ -16,6 +16,7 @@ struct yobbleApp: App {
|
||||
@StateObject private var themeManager = ThemeManager()
|
||||
@StateObject private var viewModel = LoginViewModel()
|
||||
@StateObject private var messageCenter = IncomingMessageCenter()
|
||||
@StateObject private var updateChecker = AppUpdateChecker()
|
||||
private let persistenceController = PersistenceController.shared
|
||||
|
||||
var body: some Scene {
|
||||
@ -64,11 +65,40 @@ struct yobbleApp: App {
|
||||
.preferredColorScheme(themeManager.theme.colorScheme)
|
||||
.environment(\.managedObjectContext, persistenceController.viewContext)
|
||||
.onAppear {
|
||||
updateChecker.checkForUpdatesIfNeeded()
|
||||
messageCenter.currentUserId = viewModel.userId.isEmpty ? nil : viewModel.userId
|
||||
}
|
||||
.onChange(of: viewModel.userId) { newValue in
|
||||
messageCenter.currentUserId = newValue.isEmpty ? nil : newValue
|
||||
}
|
||||
.alert(item: Binding(
|
||||
get: { updateChecker.softUpdateNotice },
|
||||
set: { newValue in
|
||||
if newValue == nil {
|
||||
updateChecker.dismissSoftUpdateIfNeeded()
|
||||
}
|
||||
}
|
||||
)) { notice in
|
||||
Alert(
|
||||
title: Text(notice.title),
|
||||
message: Text(notice.message),
|
||||
primaryButton: .default(Text(NSLocalizedString("Обновить", comment: ""))) {
|
||||
updateChecker.openAppStore()
|
||||
},
|
||||
secondaryButton: .cancel(Text(NSLocalizedString("Позже", comment: ""))) {
|
||||
updateChecker.dismissSoftUpdateIfNeeded()
|
||||
}
|
||||
)
|
||||
}
|
||||
.overlay(alignment: .center) {
|
||||
if let notice = updateChecker.forceUpdateNotice {
|
||||
ForceUpdateView(
|
||||
title: notice.title,
|
||||
message: notice.message,
|
||||
onUpdate: { updateChecker.openAppStore() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user