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
6 changes: 6 additions & 0 deletions App/Shared/Resources/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@
"It's free, with no ads and no tracking. If it's been useful, a small tip helps keep it that way." = "Es ist kostenlos, ohne Werbung und ohne Tracking. Wenn es dir nützlich war, hilft ein kleines Trinkgeld, das so zu halten.";
"Leave a tip" = "Trinkgeld geben";
"Maybe later" = "Vielleicht später";

"Send feedback" = "Feedback senden";
"Rate LogWeight on the App Store" = "LogWeight im App Store bewerten";
"Feedback emails include only your app version, iOS version, and device model. No health data is attached." = "Feedback-E-Mails enthalten nur App-Version, iOS-Version und Gerätemodell. Es werden keine Gesundheitsdaten angehängt.";
"Email address copied" = "E-Mail-Adresse kopiert";
"LogWeight feedback" = "LogWeight-Feedback";
6 changes: 6 additions & 0 deletions App/Shared/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@
"It's free, with no ads and no tracking. If it's been useful, a small tip helps keep it that way." = "It's free, with no ads and no tracking. If it's been useful, a small tip helps keep it that way.";
"Leave a tip" = "Leave a tip";
"Maybe later" = "Maybe later";

"Send feedback" = "Send feedback";
"Rate LogWeight on the App Store" = "Rate LogWeight on the App Store";
"Feedback emails include only your app version, iOS version, and device model. No health data is attached." = "Feedback emails include only your app version, iOS version, and device model. No health data is attached.";
"Email address copied" = "Email address copied";
"LogWeight feedback" = "LogWeight feedback";
6 changes: 6 additions & 0 deletions App/Shared/Resources/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@
"It's free, with no ads and no tracking. If it's been useful, a small tip helps keep it that way." = "Es gratis, sin anuncios ni seguimiento. Si te ha resultado útil, una pequeña propina ayuda a que siga siendo así.";
"Leave a tip" = "Dejar una propina";
"Maybe later" = "Quizá más tarde";

"Send feedback" = "Enviar comentarios";
"Rate LogWeight on the App Store" = "Valorar LogWeight en la App Store";
"Feedback emails include only your app version, iOS version, and device model. No health data is attached." = "Los correos de comentarios solo incluyen la versión de la app, la versión de iOS y el modelo del dispositivo. No se adjuntan datos de salud.";
"Email address copied" = "Dirección de correo copiada";
"LogWeight feedback" = "Comentarios sobre LogWeight";
6 changes: 6 additions & 0 deletions App/Shared/Resources/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@
"It's free, with no ads and no tracking. If it's been useful, a small tip helps keep it that way." = "L’application est gratuite, sans publicité ni suivi. Si elle vous a été utile, un petit pourboire aide à le rester.";
"Leave a tip" = "Laisser un pourboire";
"Maybe later" = "Plus tard";

"Send feedback" = "Envoyer un commentaire";
"Rate LogWeight on the App Store" = "Noter LogWeight sur l'App Store";
"Feedback emails include only your app version, iOS version, and device model. No health data is attached." = "Les e-mails de commentaires n'incluent que la version de l'app, la version d'iOS et le modèle de l'appareil. Aucune donnée de santé n'est jointe.";
"Email address copied" = "Adresse e-mail copiée";
"LogWeight feedback" = "Commentaire sur LogWeight";
6 changes: 6 additions & 0 deletions App/Shared/Resources/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@
"It's free, with no ads and no tracking. If it's been useful, a small tip helps keep it that way." = "È gratuita, senza pubblicità né tracciamento. Se ti è stata utile, una piccola mancia aiuta a mantenerla così.";
"Leave a tip" = "Lascia una mancia";
"Maybe later" = "Forse più tardi";

"Send feedback" = "Invia feedback";
"Rate LogWeight on the App Store" = "Valuta LogWeight sull'App Store";
"Feedback emails include only your app version, iOS version, and device model. No health data is attached." = "Le email di feedback includono solo la versione dell'app, la versione di iOS e il modello del dispositivo. Non vengono allegati dati sulla salute.";
"Email address copied" = "Indirizzo email copiato";
"LogWeight feedback" = "Feedback su LogWeight";
6 changes: 6 additions & 0 deletions App/Shared/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@
"It's free, with no ads and no tracking. If it's been useful, a small tip helps keep it that way." = "無料で、広告もトラッキングもありません。役立っていたら、少額のチップがその継続を支えます。";
"Leave a tip" = "チップを送る";
"Maybe later" = "また今度";

"Send feedback" = "フィードバックを送信";
"Rate LogWeight on the App Store" = "App StoreでLogWeightを評価";
"Feedback emails include only your app version, iOS version, and device model. No health data is attached." = "フィードバックのメールには、アプリのバージョン、iOSのバージョン、デバイスモデルのみが含まれます。健康データが添付されることはありません。";
"Email address copied" = "メールアドレスをコピーしました";
"LogWeight feedback" = "LogWeightへのフィードバック";
6 changes: 6 additions & 0 deletions App/Shared/Resources/ko.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@
"It's free, with no ads and no tracking. If it's been useful, a small tip helps keep it that way." = "광고와 추적 없이 무료로 제공됩니다. 유용하셨다면 소액의 팁이 이를 유지하는 데 도움이 됩니다.";
"Leave a tip" = "팁 남기기";
"Maybe later" = "나중에";

"Send feedback" = "피드백 보내기";
"Rate LogWeight on the App Store" = "App Store에서 LogWeight 평가하기";
"Feedback emails include only your app version, iOS version, and device model. No health data is attached." = "피드백 이메일에는 앱 버전, iOS 버전, 기기 모델만 포함됩니다. 건강 데이터는 첨부되지 않습니다.";
"Email address copied" = "이메일 주소가 복사되었습니다";
"LogWeight feedback" = "LogWeight 피드백";
6 changes: 6 additions & 0 deletions App/Shared/Resources/nl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@
"It's free, with no ads and no tracking. If it's been useful, a small tip helps keep it that way." = "Het is gratis, zonder advertenties en zonder tracking. Als het nuttig was, helpt een kleine fooi om dat zo te houden.";
"Leave a tip" = "Een fooi geven";
"Maybe later" = "Misschien later";

"Send feedback" = "Feedback sturen";
"Rate LogWeight on the App Store" = "Beoordeel LogWeight in de App Store";
"Feedback emails include only your app version, iOS version, and device model. No health data is attached." = "Feedbackmails bevatten alleen je app-versie, iOS-versie en apparaatmodel. Er worden geen gezondheidsgegevens meegestuurd.";
"Email address copied" = "E-mailadres gekopieerd";
"LogWeight feedback" = "LogWeight-feedback";
6 changes: 6 additions & 0 deletions App/Shared/Resources/pt-BR.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@
"It's free, with no ads and no tracking. If it's been useful, a small tip helps keep it that way." = "É gratuito, sem anúncios e sem rastreamento. Se tem sido útil, uma pequena gorjeta ajuda a manter assim.";
"Leave a tip" = "Deixar uma gorjeta";
"Maybe later" = "Talvez depois";

"Send feedback" = "Enviar feedback";
"Rate LogWeight on the App Store" = "Avaliar o LogWeight na App Store";
"Feedback emails include only your app version, iOS version, and device model. No health data is attached." = "Os e-mails de feedback incluem apenas a versão do app, a versão do iOS e o modelo do dispositivo. Nenhum dado de saúde é anexado.";
"Email address copied" = "Endereço de e-mail copiado";
"LogWeight feedback" = "Feedback do LogWeight";
6 changes: 6 additions & 0 deletions App/Shared/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@
"It's free, with no ads and no tracking. If it's been useful, a small tip helps keep it that way." = "它是免费的,没有广告也没有追踪。如果对你有帮助,一点小赞赏有助于保持这样。";
"Leave a tip" = "赞赏一下";
"Maybe later" = "以后再说";

"Send feedback" = "发送反馈";
"Rate LogWeight on the App Store" = "在 App Store 评价 LogWeight";
"Feedback emails include only your app version, iOS version, and device model. No health data is attached." = "反馈邮件仅包含 App 版本、iOS 版本和设备型号,不会附带任何健康数据。";
"Email address copied" = "邮件地址已复制";
"LogWeight feedback" = "LogWeight 反馈";
6 changes: 6 additions & 0 deletions App/Shared/Resources/zh-Hant.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@
"It's free, with no ads and no tracking. If it's been useful, a small tip helps keep it that way." = "它是免費的,沒有廣告也沒有追蹤。如果對你有幫助,一點小贊賞有助於保持這樣。";
"Leave a tip" = "贊賞一下";
"Maybe later" = "以後再說";

"Send feedback" = "傳送意見回饋";
"Rate LogWeight on the App Store" = "在 App Store 評價 LogWeight";
"Feedback emails include only your app version, iOS version, and device model. No health data is attached." = "意見回饋郵件僅包含 App 版本、iOS 版本和裝置型號,不會附帶任何健康資料。";
"Email address copied" = "電子郵件地址已拷貝";
"LogWeight feedback" = "LogWeight 意見回饋";
61 changes: 61 additions & 0 deletions App/Shared/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ struct SettingsView: View {
@State private var pendingReminderEnabled: Bool?

@Environment(\.dismiss) private var dismiss
#if os(iOS)
@Environment(\.openURL) private var openURL
@State private var showsEmailCopiedAlert = false
#endif

#if os(iOS)
private let reminderCoordinator = ReminderCoordinator()
Expand Down Expand Up @@ -83,6 +87,25 @@ struct SettingsView: View {
}
.accessibilityIdentifier("settings.tipjar")
}

Section("Feedback") {
Comment thread
Juuro marked this conversation as resolved.
Button {
sendFeedbackEmail()
} label: {
Label("Send feedback", systemImage: "envelope")
}
.accessibilityIdentifier("settings.feedback.email")

Link(destination: FeedbackLinks.writeReviewURL) {
Label("Rate LogWeight on the App Store", systemImage: "star")
.fixedSize(horizontal: false, vertical: true)
}
.accessibilityIdentifier("settings.feedback.rate")

Text("Feedback emails include only your app version, iOS version, and device model. No health data is attached.")
.font(.footnote)
.foregroundStyle(.secondary)
}
#endif

Section("Apple Health") {
Expand Down Expand Up @@ -124,11 +147,49 @@ struct SettingsView: View {
.task {
reminderAuthStatus = await reminderScheduler.authorizationStatus()
}
.alert("Email address copied", isPresented: $showsEmailCopiedAlert) {
} message: {
Text(FeedbackLinks.supportAddress)
}
#endif
}
}

#if os(iOS)
/// Opens a prefilled mail draft. If no mail app can handle it, copies the
/// support address so the user can write from any client.
private func sendFeedbackEmail() {
let subject = String(localized: "LogWeight feedback")
guard let url = FeedbackLinks.mailtoURL(subject: subject, diagnostics: Self.feedbackDiagnostics()) else {
copySupportAddress()
return
}
openURL(url) { accepted in
if !accepted { copySupportAddress() }
}
}

private func copySupportAddress() {
UIPasteboard.general.string = FeedbackLinks.supportAddress
showsEmailCopiedAlert = true
}

private static func feedbackDiagnostics() -> FeedbackLinks.Diagnostics {
let info = Bundle.main.infoDictionary
var systemInfo = utsname()
uname(&systemInfo)
let machine = withUnsafeBytes(of: &systemInfo.machine) { buffer in
String(decoding: buffer.prefix(while: { $0 != 0 }), as: UTF8.self)
}
let model = ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] ?? machine
return FeedbackLinks.Diagnostics(
appVersion: info?["CFBundleShortVersionString"] as? String ?? "?",
build: info?["CFBundleVersion"] as? String ?? "?",
osVersion: UIDevice.current.systemVersion,
deviceModel: model
)
}

private var reminderEnabledBinding: Binding<Bool> {
Binding(
get: { reminderEnabled },
Expand Down
8 changes: 5 additions & 3 deletions App/WatchScreenshots/WatchScreenshotTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ class WatchScreenshotTestCase: XCTestCase {

/// Launches the watch app with an in-memory store, optionally preloaded
/// with a `ScreenshotFixture` via `seed`. Display unit follows the
/// simulator's region (set via `xcodebuild test -testRegion`): DE
/// captures in kg, everything else in lb.
/// simulator's region (set via `xcodebuild test -testRegion`): only
/// `.us` is imperial (lb) — `.uk` (e.g. en-GB) still displays kg for
/// body weight despite Foundation reporting it as a distinct,
/// non-`.metric` measurement system.
func launchApp(seed: String? = nil) {
let unit = Locale.current.region?.identifier == "DE" ? "kg" : "lb"
let unit = Locale.current.measurementSystem == .us ? "lb" : "kg"
var args = ["--use-in-memory-store", "-logweight_unit_preference", unit]
if let seed {
args.append("--seed=\(seed)")
Expand Down
12 changes: 12 additions & 0 deletions App/iOS/Support/TipJarStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ final class TipJarStore {

private(set) var products: [Product] = []
private(set) var purchaseState: PurchaseState = .idle
/// Stays `true` for a week after the last tip, so the thank-you note
/// survives leaving and re-entering the screen.
private(set) var showsThankYou = TipThankYouPolicy.shouldShowThankYou()
@ObservationIgnored
private nonisolated(unsafe) var transactionListener: Task<Void, Never>?

Expand Down Expand Up @@ -52,6 +55,7 @@ final class TipJarStore {
switch try await product.purchase() {
case .success(.verified(let transaction)):
await transaction.finish()
recordTip(transaction)
purchaseState = .thankYou(tip)
case .success(.unverified):
purchaseState = .failed
Expand All @@ -69,7 +73,15 @@ final class TipJarStore {
guard case .verified(let transaction) = result else { return }
await transaction.finish()
if let tip = TipJarProduct(rawValue: transaction.productID) {
recordTip(transaction)
purchaseState = .thankYou(tip)
}
}

private func recordTip(_ transaction: Transaction) {
TipThankYouPolicy.recordTip(at: transaction.purchaseDate)
// Ensure the thank-you is visible immediately after a verified purchase,
// even if the local clock is behind the transaction’s purchaseDate.
showsThankYou = TipThankYouPolicy.shouldShowThankYou(now: transaction.purchaseDate)
}
Comment thread
Copilot marked this conversation as resolved.
}
2 changes: 1 addition & 1 deletion App/iOS/Views/TipJarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ struct TipJarView: View {
.foregroundStyle(.secondary)
}

if case .thankYou = store.purchaseState {
if store.showsThankYou {
Section {
Label("Thank you for supporting LogWeight!", systemImage: "heart.fill")
.foregroundStyle(.pink)
Expand Down
10 changes: 6 additions & 4 deletions App/iOSScreenshots/ScreenshotTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ class ScreenshotTestCase: XCTestCase {
/// the calling shell don't cross into the simulator-hosted test process,
/// so locale is driven by `xcodebuild test -testLanguage -testRegion`
/// instead (see Tools/CaptureScene.sh and Tools/CaptureStoreScreenshots.sh).
/// Display unit follows that same region: DE captures in kg, everything
/// else in lb, passed via the `-logweight_unit_preference` NSUserDefaults
/// argument (crosses into the app process the same way `-AppleLanguages` did).
/// Display unit follows that region: only `.us` is imperial (lb) — `.uk`
/// (e.g. en-GB) still displays kg for body weight despite Foundation
/// reporting it as a distinct, non-`.metric` measurement system — passed
/// via the `-logweight_unit_preference` NSUserDefaults argument (crosses
/// into the app process the same way `-AppleLanguages` did).
func launchApp(seed: String? = nil, extraArguments: [String] = []) {
let unit = Locale.current.region?.identifier == "DE" ? "kg" : "lb"
let unit = Locale.current.measurementSystem == .us ? "lb" : "kg"
var args = [
"--use-in-memory-store",
"--skip-splash",
Expand Down
16 changes: 13 additions & 3 deletions App/iOSUITests/UITestNavigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,20 @@ extension XCUIApplication {
}
}

/// Localized "History" tab title, one per `App/Shared/Resources/*.lproj/Localizable.strings`
/// entry — needed because iPadOS 18's regular-width tab bar floats as a
/// top capsule outside the `tabBars` accessibility container and drops the
/// `tab.history` accessibility identifier entirely, so on iPad every
/// locale falls through to a label match.
private static let historyTabLabels = [
"History", "Verlauf", "Historique", "Historial", "Cronologia",
"Histórico", "履歴", "기록", "历史", "歷史", "Geschiedenis",
]

/// Opens the History tab on iPhone and iPad, in whatever locale the
/// simulator is running (see Tools/CaptureStoreScreenshots.sh's
/// `-testLanguage`/`-testRegion`) — falls back through stable identifier,
/// localized label (English and German), and tab index, in that order.
/// localized label (all supported locales), and tab index, in that order.
/// iPadOS 18's regular-width tab bar floats as a top capsule outside the
/// `tabBars` accessibility container, so the last fallbacks search `app.buttons` directly.
func openHistoryTab(file: StaticString = #file, line: UInt = #line) {
Expand All @@ -47,7 +57,7 @@ extension XCUIApplication {
return
}

for label in ["History", "Verlauf"] {
for label in Self.historyTabLabels {
let byLabel = tabBars.buttons[label]
if byLabel.waitForExistence(timeout: 2) {
tapTabBarButton(byLabel.firstMatch)
Expand All @@ -67,7 +77,7 @@ extension XCUIApplication {
return
}

for label in ["History", "Verlauf"] {
for label in Self.historyTabLabels {
let byLabel = buttons[label]
if byLabel.waitForExistence(timeout: 2) {
tapTabBarButton(byLabel.firstMatch)
Expand Down
2 changes: 1 addition & 1 deletion Config/Version.xcconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Build number (CFBundleVersion). Auto-incremented by CI on green runs.
// See .github/workflows/ci.yml and Tools/bump-build-number.sh.
CURRENT_PROJECT_VERSION = 1
CURRENT_PROJECT_VERSION = 19
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public enum SettingsKey {
public static let reminderMinute = "logweight_reminder_minute"
public static let successfulEntryCount = "logweight_successful_entry_count"
public static let tipPromptShown = "logweight_tip_prompt_shown"
public static let lastTipPurchaseDate = "logweight_last_tip_purchase_date"
public static let schemaVersion = "logweight_settings_schema_version"
}

Expand Down
Loading
Loading