Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 45 additions & 7 deletions GdeiAssistant-iOS/Core/Config/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,63 @@ final class AppEnvironment: ObservableObject {
let isDebug: Bool
let clientType: String

var allowsRuntimeDebugOptions: Bool {
isDebug && AppConstants.API.allowsRuntimeDebugOptions
}

init(
networkEnvironment: NetworkEnvironment,
dataSourceMode: DataSourceMode,
isDebug: Bool? = nil,
clientType: String? = nil
) {
self.networkEnvironment = networkEnvironment
self.baseURL = networkEnvironment.baseURL
self.dataSourceMode = dataSourceMode
self.isDebug = isDebug ?? _isDebugAssertConfiguration()
let resolvedIsDebug = isDebug ?? _isDebugAssertConfiguration()
self.isDebug = resolvedIsDebug
self.clientType = clientType ?? AppConstants.API.clientType
self.networkEnvironment = Self.sanitizedNetworkEnvironment(
networkEnvironment,
isDebug: resolvedIsDebug
)
self.baseURL = self.networkEnvironment.baseURL
self.dataSourceMode = Self.sanitizedDataSourceMode(
dataSourceMode,
isDebug: resolvedIsDebug
)
}

func updateDataSourceMode(_ mode: DataSourceMode) {
dataSourceMode = mode
dataSourceMode = sanitizedDataSourceMode(mode)
}

func updateNetworkEnvironment(_ environment: NetworkEnvironment) {
networkEnvironment = environment
baseURL = environment.baseURL
let nextEnvironment = sanitizedNetworkEnvironment(environment)
networkEnvironment = nextEnvironment
baseURL = nextEnvironment.baseURL
}

private func sanitizedDataSourceMode(_ mode: DataSourceMode) -> DataSourceMode {
Self.sanitizedDataSourceMode(mode, isDebug: isDebug)
}

private func sanitizedNetworkEnvironment(_ environment: NetworkEnvironment) -> NetworkEnvironment {
Self.sanitizedNetworkEnvironment(environment, isDebug: isDebug)
}

private static func allowsRuntimeDebugOptions(isDebug: Bool) -> Bool {
isDebug && AppConstants.API.allowsRuntimeDebugOptions
}

private static func sanitizedDataSourceMode(
_ mode: DataSourceMode,
isDebug: Bool
) -> DataSourceMode {
allowsRuntimeDebugOptions(isDebug: isDebug) ? mode : .remote
}

private static func sanitizedNetworkEnvironment(
_ environment: NetworkEnvironment,
isDebug: Bool
) -> NetworkEnvironment {
allowsRuntimeDebugOptions(isDebug: isDebug) ? environment : .prod

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor the configured staging environment

In a build/config that sets GDEIDefaultNetworkEnvironment=staging with runtime debug options disabled (for example Configs/Staging.xcconfig sets staging and GDEI_ALLOW_RUNTIME_DEBUG_OPTIONS = NO), AppRouter passes that selected environment into AppEnvironment, but this ternary replaces every disallowed-debug environment with .prod. That makes staging builds call production APIs instead of the configured staging base URL; mock data should be forced off without discarding the build-selected network environment.

Useful? React with 👍 / 👎.

}
}
2 changes: 1 addition & 1 deletion GdeiAssistant-iOS/Core/Config/AppLanguage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ enum AppLanguage: String, CaseIterable, Identifiable {
case .simplifiedChinese:
return "简体中文"
case .traditionalChineseHongKong:
return "繁體中文(香港)"
return "繁體中文(港澳)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Map Macau locales to the new Hong Kong/Macau option

With this option now labelled 港澳, Macau locales still won't select it: language(from:) only maps zh-HK to this case, while zh-Hant-MO is routed by the generic hant branch to Taiwan and zh-MO falls through to simplified Chinese. A device whose preferred language is Macau Traditional Chinese therefore won't get the Hong Kong/Macau localization automatically, so the Macau identifiers need to be added to this branch when broadening the label.

Useful? React with 👍 / 👎.

case .traditionalChineseTaiwan:
return "繁體中文(台灣)"
case .english:
Expand Down
11 changes: 8 additions & 3 deletions GdeiAssistant-iOS/Features/Home/Views/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ struct HomeView: View {
@StateObject private var viewModel: HomeViewModel
@EnvironmentObject private var container: AppContainer
@Environment(\.colorScheme) private var colorScheme
private let entryColumns = [
GridItem(.adaptive(minimum: 72, maximum: 96), spacing: 8)
]

init(viewModel: HomeViewModel) {
_viewModel = StateObject(wrappedValue: viewModel)
Expand Down Expand Up @@ -67,7 +70,7 @@ struct HomeView: View {
}

LazyVGrid(
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 4),
columns: entryColumns,
spacing: 16
) {
ForEach(entries) { entry in
Expand All @@ -88,9 +91,11 @@ struct HomeView: View {

Text(entry.title)
.font(.caption2)
.lineLimit(1)
.minimumScaleFactor(0.8)
.lineLimit(2)
.minimumScaleFactor(0.85)
.multilineTextAlignment(.center)
.foregroundStyle(DSColor.title)
.frame(maxWidth: .infinity)
}
.accessibilityElement(children: .combine)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ private struct MarketplaceStateChangeContext: Identifiable {
case .offShelf:
return localizedString("marketplace.stateOffShelf")
case .sold:
return localizedString("marketplace.stateSold")
return localizedString("marketplace.stateMarkedSold")
case .selling:
return localizedString("marketplace.stateRelist")
case .systemDeleted:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ final class SettingsViewModel: ObservableObject {
environment.isDebug
}

var allowsRuntimeDebugOptions: Bool {
environment.allowsRuntimeDebugOptions
}

var modeDisplayText: String {
environment.dataSourceMode.displayName
}
Expand All @@ -42,15 +46,15 @@ final class SettingsViewModel: ObservableObject {
}

func updateMockEnabled(_ isEnabled: Bool) {
guard environment.isDebug else { return }
guard environment.allowsRuntimeDebugOptions else { return }

preferences.setUseMockData(isEnabled)
environment.updateDataSourceMode(isEnabled ? .mock : .remote)
showReloadHint = true
}

func updateNetworkEnvironment(_ environment: NetworkEnvironment) {
guard self.environment.isDebug else { return }
guard self.environment.allowsRuntimeDebugOptions else { return }

preferences.setNetworkEnvironment(environment)
self.environment.updateNetworkEnvironment(environment)
Expand Down
52 changes: 25 additions & 27 deletions GdeiAssistant-iOS/Features/Profile/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,38 @@ struct SettingsView: View {

var body: some View {
List {
Section {
Toggle(LocalizedStringKey("settings.useMockData"), isOn: mockBinding)
.disabled(!viewModel.isDebug)

Text(LocalizedStringKey(viewModel.isDebug ? "settings.mockDataEnabled" : "settings.mockDataDisabled"))
.font(.footnote)
.foregroundStyle(DSColor.subtitle)
if viewModel.allowsRuntimeDebugOptions {
Section {
Toggle(LocalizedStringKey("settings.useMockData"), isOn: mockBinding)

if viewModel.showReloadHint {
Text(LocalizedStringKey("settings.reloadHint"))
Text(LocalizedStringKey("settings.mockDataEnabled"))
.font(.footnote)
.foregroundStyle(DSColor.warning)
}
} header: {
Text(LocalizedStringKey("settings.debugDataSource"))
}
.foregroundStyle(DSColor.subtitle)

Section {
Picker(LocalizedStringKey("settings.apiEnvironmentLabel"), selection: networkEnvironmentBinding) {
ForEach(NetworkEnvironment.allCases, id: \.self) { environment in
Text(environment.displayName).tag(environment)
if viewModel.showReloadHint {
Text(LocalizedStringKey("settings.reloadHint"))
.font(.footnote)
.foregroundStyle(DSColor.warning)
}
} header: {
Text(LocalizedStringKey("settings.debugDataSource"))
}
.pickerStyle(.segmented)
.disabled(!viewModel.isDebug)

Text(LocalizedStringKey(viewModel.isDebug ? "settings.apiDebugHint" : "settings.apiReleaseHint"))
.font(.footnote)
.foregroundStyle(DSColor.subtitle)
} header: {
Text(LocalizedStringKey("settings.apiEnvironment"))
}
Section {
Picker(LocalizedStringKey("settings.apiEnvironmentLabel"), selection: networkEnvironmentBinding) {
ForEach(NetworkEnvironment.allCases, id: \.self) { environment in
Text(environment.displayName).tag(environment)
}
}
.pickerStyle(.segmented)

Text(LocalizedStringKey("settings.apiDebugHint"))
.font(.footnote)
.foregroundStyle(DSColor.subtitle)
} header: {
Text(LocalizedStringKey("settings.apiEnvironment"))
}

if viewModel.isDebug {
Section {
infoRow(title: "networkEnvironment", value: viewModel.networkEnvironmentText)
infoRow(title: "baseURL", value: viewModel.baseURLText)
Expand Down
7 changes: 1 addition & 6 deletions GdeiAssistant-iOS/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -973,11 +973,6 @@
"messages.systemNoticeLoadFailed" = "Failed to load announcements";
"messages.interactionLoadFailed" = "Failed to load interactions";
"messages.updateStatusFailed" = "Failed to update status";
"messages.loadFailed" = "Failed to load messages";
"messages.newsLoadFailed" = "Failed to load news";
"messages.systemNoticeLoadFailed" = "Failed to load announcements";
"messages.interactionLoadFailed" = "Failed to load interactions";
"messages.updateStatusFailed" = "Failed to update status";

// MARK: - Marketplace
"marketplace.search" = "Search marketplace";
Expand Down Expand Up @@ -1008,7 +1003,7 @@
"marketplace.confirmSelling" = "Confirm relist";
"marketplace.confirm" = "Confirm";
"marketplace.stateOffShelf" = "Item removed";
"marketplace.stateSold" = "Item marked as sold";
"marketplace.stateMarkedSold" = "Item marked as sold";
"marketplace.stateRelist" = "Item relisted";
"marketplace.stateUpdated" = "Status updated";
"marketplace.actionFailed" = "Operation failed";
Expand Down
2 changes: 1 addition & 1 deletion GdeiAssistant-iOS/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1003,7 +1003,7 @@
"marketplace.confirmSelling" = "再出品を確認";
"marketplace.confirm" = "確認";
"marketplace.stateOffShelf" = "商品を出品停止にしました";
"marketplace.stateSold" = "商品を売却済みにしました";
"marketplace.stateMarkedSold" = "商品を売却済みにしました";
"marketplace.stateRelist" = "商品を再出品しました";
"marketplace.stateUpdated" = "ステータスを更新しました";
"marketplace.actionFailed" = "操作に失敗しました";
Expand Down
2 changes: 1 addition & 1 deletion GdeiAssistant-iOS/Resources/ko.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1003,7 +1003,7 @@
"marketplace.confirmSelling" = "다시 올리기 확인";
"marketplace.confirm" = "확인";
"marketplace.stateOffShelf" = "상품이 내려갔습니다";
"marketplace.stateSold" = "상품이 판매완료로 표시되었습니다";
"marketplace.stateMarkedSold" = "상품이 판매완료로 표시되었습니다";
"marketplace.stateRelist" = "상품이 다시 올라갔습니다";
"marketplace.stateUpdated" = "상태가 업데이트되었습니다";
"marketplace.actionFailed" = "작업 실패";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1003,7 +1003,7 @@
"marketplace.confirmSelling" = "確認上架";
"marketplace.confirm" = "確認";
"marketplace.stateOffShelf" = "商品已下架";
"marketplace.stateSold" = "商品已標記為售出";
"marketplace.stateMarkedSold" = "商品已標記為售出";
"marketplace.stateRelist" = "商品已重新上架";
"marketplace.stateUpdated" = "狀態已更新";
"marketplace.actionFailed" = "操作失敗";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -973,11 +973,6 @@
"messages.systemNoticeLoadFailed" = "系统公告加载失败";
"messages.interactionLoadFailed" = "互动消息加载失败";
"messages.updateStatusFailed" = "更新消息状态失败";
"messages.loadFailed" = "加载资讯信息失败";
"messages.newsLoadFailed" = "新闻加载失败";
"messages.systemNoticeLoadFailed" = "系统公告加载失败";
"messages.interactionLoadFailed" = "互动消息加载失败";
"messages.updateStatusFailed" = "更新消息状态失败";

// MARK: - Marketplace
"marketplace.search" = "搜索二手交易";
Expand Down Expand Up @@ -1008,7 +1003,7 @@
"marketplace.confirmSelling" = "确认上架";
"marketplace.confirm" = "确认";
"marketplace.stateOffShelf" = "商品已下架";
"marketplace.stateSold" = "商品已标记为售出";
"marketplace.stateMarkedSold" = "商品已标记为售出";
"marketplace.stateRelist" = "商品已重新上架";
"marketplace.stateUpdated" = "状态已更新";
"marketplace.actionFailed" = "操作失败";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1003,7 +1003,7 @@
"marketplace.confirmSelling" = "確認上架";
"marketplace.confirm" = "確認";
"marketplace.stateOffShelf" = "商品已下架";
"marketplace.stateSold" = "商品已標記為售出";
"marketplace.stateMarkedSold" = "商品已標記為售出";
"marketplace.stateRelist" = "商品已重新上架";
"marketplace.stateUpdated" = "狀態已更新";
"marketplace.actionFailed" = "操作失敗";
Expand Down
6 changes: 6 additions & 0 deletions GdeiAssistant-iOSTests/Localization/AppLanguageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ final class AppLanguageTests: XCTestCase {
XCTAssertEqual(AppLanguage.normalizedIdentifier(from: "ko"), "ko")
}

func testNativeNamesSeparateHongKongMacauAndTaiwanTraditionalChinese() {
XCTAssertEqual(AppLanguage.traditionalChineseHongKong.nativeName, "繁體中文(港澳)")
XCTAssertEqual(AppLanguage.traditionalChineseTaiwan.nativeName, "繁體中文(台灣)")
XCTAssertFalse(AppLanguage.allCases.map(\.nativeName).contains("繁體中文(香港)"))
}

func testNormalizeMapsLocaleVariantsToSupportedIdentifiers() {
XCTAssertEqual(AppLanguage.normalizedIdentifier(from: "zh-Hans"), "zh-CN")
XCTAssertEqual(AppLanguage.normalizedIdentifier(from: "zh-Hans-CN"), "zh-CN")
Expand Down
45 changes: 45 additions & 0 deletions GdeiAssistant-iOSTests/Profile/SettingsViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,51 @@ final class SettingsViewModelTests: XCTestCase {
XCTAssertFalse(viewModel.showReloadHint)
}

func testReleaseEnvironmentSanitizesDebugDataSourceInputs() {
let defaults = makeDefaults(testName: #function)
let preferences = UserPreferences(defaults: defaults)
let environment = AppEnvironment(
networkEnvironment: .dev,
dataSourceMode: .mock,
isDebug: false,
clientType: "IOS"
)
let viewModel = SettingsViewModel(environment: environment, preferences: preferences)
TestLifetimeRetainer.retain(viewModel)

XCTAssertFalse(viewModel.allowsRuntimeDebugOptions)
XCTAssertEqual(environment.networkEnvironment, .prod)
XCTAssertEqual(environment.baseURL, NetworkEnvironment.prod.baseURL)
XCTAssertEqual(environment.dataSourceMode, .remote)
XCTAssertFalse(viewModel.useMockData)

environment.updateNetworkEnvironment(.staging)
environment.updateDataSourceMode(.mock)

XCTAssertEqual(environment.networkEnvironment, .prod)
XCTAssertEqual(environment.baseURL, NetworkEnvironment.prod.baseURL)
XCTAssertEqual(environment.dataSourceMode, .remote)
}

func testUpdateMockEnabledIsIgnoredOutsideDebugBuilds() {
let defaults = makeDefaults(testName: #function)
let preferences = UserPreferences(defaults: defaults)
let environment = AppEnvironment(
networkEnvironment: .prod,
dataSourceMode: .remote,
isDebug: false,
clientType: "IOS"
)
let viewModel = SettingsViewModel(environment: environment, preferences: preferences)
TestLifetimeRetainer.retain(viewModel)

viewModel.updateMockEnabled(true)

XCTAssertEqual(environment.dataSourceMode, .remote)
XCTAssertEqual(preferences.currentDataSourceMode, .remote)
XCTAssertFalse(viewModel.showReloadHint)
}

func testUpdateMockEnabledSwitchesEnvironmentModeInDebug() {
let defaults = makeDefaults(testName: #function)
let preferences = UserPreferences(defaults: defaults)
Expand Down
Loading