Compare commits
No commits in common. "2679f31c4efdf7c2c7d3466eb0a458947ff1e2ec" and "84a8706b0a46ba36478d8d23866b19ef3b8a4dbd" have entirely different histories.
2679f31c4e
...
84a8706b0a
@ -120,42 +120,6 @@
|
|||||||
"Email не подтверждён. Подтвердите, чтобы активировать дополнительные проверки." : {
|
"Email не подтверждён. Подтвердите, чтобы активировать дополнительные проверки." : {
|
||||||
"comment" : "Описание необходимости подтверждения email"
|
"comment" : "Описание необходимости подтверждения email"
|
||||||
},
|
},
|
||||||
"ForceUpdate.Message" : {
|
|
||||||
"comment" : "Force update alert message",
|
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "This version is no longer supported. Please update the app."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ru" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Эта версия приложения устарела и больше не поддерживается. Пожалуйста, обновите приложение."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ForceUpdate.Title" : {
|
|
||||||
"comment" : "Force update alert title",
|
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Update required"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ru" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Требуется обновление"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Fun Fest" : {
|
"Fun Fest" : {
|
||||||
"comment" : "Fun Fest",
|
"comment" : "Fun Fest",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -240,42 +204,6 @@
|
|||||||
},
|
},
|
||||||
"Qr" : {
|
"Qr" : {
|
||||||
|
|
||||||
},
|
|
||||||
"SoftUpdate.Message" : {
|
|
||||||
"comment" : "Soft update alert message",
|
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "A new version is available. Some features may stop working correctly soon."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ru" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Вышла новая версия приложения. Некоторые функции могут скоро работать некорректно."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SoftUpdate.Title" : {
|
|
||||||
"comment" : "Soft update alert title",
|
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Update available"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ru" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Доступно обновление"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Yobble" : {
|
"Yobble" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -582,9 +510,6 @@
|
|||||||
},
|
},
|
||||||
"Выключено" : {
|
"Выключено" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Вышла новая версия приложения с улучшениями и исправлениями." : {
|
|
||||||
"comment" : "Soft update alert message"
|
|
||||||
},
|
},
|
||||||
"Где найти сохранённые черновики?" : {
|
"Где найти сохранённые черновики?" : {
|
||||||
"comment" : "FAQ question: drafts"
|
"comment" : "FAQ question: drafts"
|
||||||
@ -647,9 +572,6 @@
|
|||||||
},
|
},
|
||||||
"Для начала, мы рекомендуем настроить параметры безопасности вашего аккаунта." : {
|
"Для начала, мы рекомендуем настроить параметры безопасности вашего аккаунта." : {
|
||||||
|
|
||||||
},
|
|
||||||
"Для продолжения работы необходимо обновить приложение до последней версии." : {
|
|
||||||
"comment" : "Need update alert message"
|
|
||||||
},
|
},
|
||||||
"Добавить в контакты" : {
|
"Добавить в контакты" : {
|
||||||
"comment" : "Message profile add to contacts title"
|
"comment" : "Message profile add to contacts title"
|
||||||
@ -683,9 +605,6 @@
|
|||||||
"Дополнительные действия." : {
|
"Дополнительные действия." : {
|
||||||
"comment" : "Message profile more action description"
|
"comment" : "Message profile more action description"
|
||||||
},
|
},
|
||||||
"Доступно обновление" : {
|
|
||||||
"comment" : "Soft update alert title"
|
|
||||||
},
|
|
||||||
"Другие устройства (%d)" : {
|
"Другие устройства (%d)" : {
|
||||||
"comment" : "Заголовок секции других устройств с количеством"
|
"comment" : "Заголовок секции других устройств с количеством"
|
||||||
},
|
},
|
||||||
@ -1848,12 +1767,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Обновить приложение" : {
|
|
||||||
|
|
||||||
},
|
|
||||||
"Обновление обязательно" : {
|
|
||||||
"comment" : "Need update alert title"
|
|
||||||
},
|
|
||||||
"Обратная связь" : {
|
"Обратная связь" : {
|
||||||
"comment" : "feedback: navigation title",
|
"comment" : "feedback: navigation title",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2759,9 +2672,6 @@
|
|||||||
"Рейтинг собеседника" : {
|
"Рейтинг собеседника" : {
|
||||||
"comment" : "Message profile rating title"
|
"comment" : "Message profile rating title"
|
||||||
},
|
},
|
||||||
"Рекомендуется обновление" : {
|
|
||||||
"comment" : "Force update alert title"
|
|
||||||
},
|
|
||||||
"Рожки и ножки у сообщений" : {
|
"Рожки и ножки у сообщений" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -3321,9 +3231,6 @@
|
|||||||
},
|
},
|
||||||
"Экспериментальная поддержка iOS 15/16" : {
|
"Экспериментальная поддержка iOS 15/16" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Эта версия приложения устарела. Некоторые функции могут работать некорректно." : {
|
|
||||||
"comment" : "Force update alert message"
|
|
||||||
},
|
},
|
||||||
"Это устройство" : {
|
"Это устройство" : {
|
||||||
"comment" : "Заголовок секции текущего устройства"
|
"comment" : "Заголовок секции текущего устройства"
|
||||||
|
|||||||
@ -1,186 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
struct AppUpdateNotice: Identifiable {
|
|
||||||
enum Kind {
|
|
||||||
case need
|
|
||||||
case force
|
|
||||||
case soft
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = UUID()
|
|
||||||
let kind: Kind
|
|
||||||
let appStoreURL: URL
|
|
||||||
|
|
||||||
var title: String {
|
|
||||||
switch kind {
|
|
||||||
case .need:
|
|
||||||
return NSLocalizedString("Обновление обязательно", comment: "Need update alert title")
|
|
||||||
case .force:
|
|
||||||
return NSLocalizedString("Рекомендуется обновление", comment: "Force update alert title")
|
|
||||||
case .soft:
|
|
||||||
return NSLocalizedString("Доступно обновление", comment: "Soft update alert title")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var message: String {
|
|
||||||
switch kind {
|
|
||||||
case .need:
|
|
||||||
return NSLocalizedString("Для продолжения работы необходимо обновить приложение до последней версии.", comment: "Need update alert message")
|
|
||||||
case .force:
|
|
||||||
return NSLocalizedString("Эта версия приложения устарела. Некоторые функции могут работать некорректно.", comment: "Force update alert message")
|
|
||||||
case .soft:
|
|
||||||
return NSLocalizedString("Вышла новая версия приложения с улучшениями и исправлениями.", comment: "Soft update alert message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class AppUpdateChecker: ObservableObject {
|
|
||||||
@AppStorage("appIsBlocked") private var isAppBlocked: Bool = false
|
|
||||||
@AppStorage("lastCheckedAppBuild") private var lastCheckedAppBuild: Int = 0
|
|
||||||
|
|
||||||
@Published private(set) var needUpdateNotice: AppUpdateNotice?
|
|
||||||
@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(link overrideURL: URL? = nil) {
|
|
||||||
guard let url = overrideURL
|
|
||||||
?? needUpdateNotice?.appStoreURL
|
|
||||||
?? 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/\(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
|
|
||||||
}
|
|
||||||
|
|
||||||
needUpdateNotice = nil
|
|
||||||
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 requiresNeedUpdate = buildNumber <= config.notSupportedBuild
|
|
||||||
if requiresNeedUpdate {
|
|
||||||
isAppBlocked = true
|
|
||||||
needUpdateNotice = AppUpdateNotice(kind: .need, appStoreURL: appStoreURL)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
isAppBlocked = false
|
|
||||||
}
|
|
||||||
|
|
||||||
let requiresForcedUpdate = buildNumber < config.minSupportedBuild
|
|
||||||
if requiresForcedUpdate {
|
|
||||||
softUpdateNotice = AppUpdateNotice(kind: .force, appStoreURL: appStoreURL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if buildNumber < config.recommendedBuild && config.recommendedBuild != lastCheckedAppBuild {
|
|
||||||
// lastCheckedAppBuild = config.recommendedBuild
|
|
||||||
softUpdateNotice = AppUpdateNotice(kind: .soft, 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 {
|
|
||||||
let schemaVersion: Int
|
|
||||||
let notSupportedBuild: Int
|
|
||||||
let minSupportedBuild: Int
|
|
||||||
let recommendedBuild: Int
|
|
||||||
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 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)
|
|
||||||
if let urlString = try container.decodeIfPresent(String.self, forKey: .appStoreURL) {
|
|
||||||
appStoreURL = URL(string: urlString)
|
|
||||||
} else {
|
|
||||||
appStoreURL = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,11 +7,8 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
class LoginViewModel: ObservableObject {
|
class LoginViewModel: ObservableObject {
|
||||||
// @AppStorage("appIsBlocked") private var isAppBlocked: Bool = false
|
|
||||||
|
|
||||||
@Published var username: String = ""
|
@Published var username: String = ""
|
||||||
@Published var userId: String = ""
|
@Published var userId: String = ""
|
||||||
@Published var password: String = ""
|
@Published var password: String = ""
|
||||||
|
|||||||
@ -1,62 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct NeedUpdateView: View {
|
|
||||||
let title: String
|
|
||||||
let message: String
|
|
||||||
let onUpdate: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
LinearGradient(
|
|
||||||
colors: [Color(.systemBackground), Color(.systemGray6)],
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
VStack(spacing: 24) {
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Image(systemName: "exclamationmark.triangle.fill")
|
|
||||||
.font(.system(size: 56, weight: .bold))
|
|
||||||
.foregroundColor(.orange)
|
|
||||||
.accessibilityHidden(true)
|
|
||||||
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
Text(title)
|
|
||||||
.font(.title2.weight(.semibold))
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
|
|
||||||
Text(message)
|
|
||||||
.font(.body)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: onUpdate) {
|
|
||||||
Text(NSLocalizedString("Обновить приложение", comment: ""))
|
|
||||||
.font(.headline)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding()
|
|
||||||
.background(Color.accentColor)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.cornerRadius(14)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(32)
|
|
||||||
.frame(maxWidth: 480)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.accessibilityElement(children: .contain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NeedUpdateView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
NeedUpdateView(title: "Требуется обновление",
|
|
||||||
message: "Эта версия приложения устарела и больше не поддерживается.",
|
|
||||||
onUpdate: {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AppConfig {
|
struct AppConfig {
|
||||||
static var DEBUG: Bool = false
|
static var DEBUG: Bool = true
|
||||||
//static let SERVICE = Bundle.main.bundleIdentifier ?? "default.service"
|
//static let SERVICE = Bundle.main.bundleIdentifier ?? "default.service"
|
||||||
static let PROTOCOL = "https"
|
static let PROTOCOL = "https"
|
||||||
static let API_SERVER = "\(PROTOCOL)://api.yobble.org"
|
static let API_SERVER = "\(PROTOCOL)://api.yobble.org"
|
||||||
|
|||||||
@ -16,19 +16,10 @@ struct yobbleApp: App {
|
|||||||
@StateObject private var themeManager = ThemeManager()
|
@StateObject private var themeManager = ThemeManager()
|
||||||
@StateObject private var viewModel = LoginViewModel()
|
@StateObject private var viewModel = LoginViewModel()
|
||||||
@StateObject private var messageCenter = IncomingMessageCenter()
|
@StateObject private var messageCenter = IncomingMessageCenter()
|
||||||
@StateObject private var updateChecker = AppUpdateChecker()
|
|
||||||
private let persistenceController = PersistenceController.shared
|
private let persistenceController = PersistenceController.shared
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
Group {
|
|
||||||
if let notice = updateChecker.needUpdateNotice {
|
|
||||||
NeedUpdateView(
|
|
||||||
title: notice.title,
|
|
||||||
message: notice.message,
|
|
||||||
onUpdate: { updateChecker.openAppStore(link: notice.appStoreURL) }
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
Group {
|
Group {
|
||||||
if viewModel.isInitialLoading {
|
if viewModel.isInitialLoading {
|
||||||
@ -68,33 +59,11 @@ struct yobbleApp: App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.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(link: notice.appStoreURL)
|
|
||||||
},
|
|
||||||
secondaryButton: .cancel(Text(NSLocalizedString("Позже", comment: ""))) {
|
|
||||||
updateChecker.dismissSoftUpdateIfNeeded()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.environmentObject(messageCenter)
|
.environmentObject(messageCenter)
|
||||||
.environmentObject(themeManager)
|
.environmentObject(themeManager)
|
||||||
.preferredColorScheme(themeManager.theme.colorScheme)
|
.preferredColorScheme(themeManager.theme.colorScheme)
|
||||||
.environment(\.managedObjectContext, persistenceController.viewContext)
|
.environment(\.managedObjectContext, persistenceController.viewContext)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
updateChecker.checkForUpdatesIfNeeded()
|
|
||||||
messageCenter.currentUserId = viewModel.userId.isEmpty ? nil : viewModel.userId
|
messageCenter.currentUserId = viewModel.userId.isEmpty ? nil : viewModel.userId
|
||||||
}
|
}
|
||||||
.onChange(of: viewModel.userId) { newValue in
|
.onChange(of: viewModel.userId) { newValue in
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user