Fix #37: Privacy-Compliance + Onboarding-Kontaktpflicht

- NSContactsUsageDescription + NSPhotoLibraryUsageDescription in Info.plist
  ergänzt (App-Store-Review-Compliance für import Contacts/PhotosUI)
- Onboarding: Überspringen-Button entfernt, mindestens 1 Kontakt erforderlich
- Hinweistext wenn Kontaktliste leer: erklärt warum Weiter gesperrt ist
- Alert wenn Picker-Auswahl das Free-Tier-Limit von 3 überschreitet
- Lokalisierung (DE+EN) für alle neuen Strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 09:16:00 +02:00
parent 2c274ff4ae
commit e4c2293bec
3 changed files with 4405 additions and 4357 deletions
+4
View File
@@ -715,6 +715,8 @@
INFOPLIST_KEY_CFBundleDisplayName = nahbar; INFOPLIST_KEY_CFBundleDisplayName = nahbar;
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "nahbar liest deine Kalender, damit du beim Erstellen eines Termins den richtigen Kalender wählen kannst."; INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "nahbar liest deine Kalender, damit du beim Erstellen eines Termins den richtigen Kalender wählen kannst.";
INFOPLIST_KEY_NSCalendarsWriteOnlyAccessUsageDescription = "nahbar erstellt Kalendereinträge für geplante Treffen"; INFOPLIST_KEY_NSCalendarsWriteOnlyAccessUsageDescription = "nahbar erstellt Kalendereinträge für geplante Treffen";
INFOPLIST_KEY_NSContactsUsageDescription = "nahbar öffnet den systemseitigen Kontakte-Picker, damit du Personen schnell aus deinem Adressbuch hinzufügen kannst. Die App liest deine Kontakte nicht selbstständig aus.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "nahbar öffnet den systemseitigen Foto-Picker, damit du ein Profilbild für eine Person auswählen kannst.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -756,6 +758,8 @@
INFOPLIST_KEY_CFBundleDisplayName = nahbar; INFOPLIST_KEY_CFBundleDisplayName = nahbar;
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "nahbar liest deine Kalender, damit du beim Erstellen eines Termins den richtigen Kalender wählen kannst."; INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "nahbar liest deine Kalender, damit du beim Erstellen eines Termins den richtigen Kalender wählen kannst.";
INFOPLIST_KEY_NSCalendarsWriteOnlyAccessUsageDescription = "nahbar erstellt Kalendereinträge für geplante Treffen"; INFOPLIST_KEY_NSCalendarsWriteOnlyAccessUsageDescription = "nahbar erstellt Kalendereinträge für geplante Treffen";
INFOPLIST_KEY_NSContactsUsageDescription = "nahbar öffnet den systemseitigen Kontakte-Picker, damit du Personen schnell aus deinem Adressbuch hinzufügen kannst. Die App liest deine Kontakte nicht selbstständig aus.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "nahbar öffnet den systemseitigen Foto-Picker, damit du ein Profilbild für eine Person auswählen kannst.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+35
View File
@@ -228,6 +228,16 @@
} }
} }
}, },
"%lld Kontakte wurden nicht hinzugefügt. Im Free-Tier kannst du beim Onboarding bis zu 3 Personen auswählen." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld contacts were not added. In the Free Tier, you can select up to 3 people during onboarding."
}
}
}
},
"%lld Schritte" : { "%lld Schritte" : {
"comment" : "SettingsView tour step count label (e.g. '6 Schritte')", "comment" : "SettingsView tour step count label (e.g. '6 Schritte')",
"extractionState" : "stale", "extractionState" : "stale",
@@ -364,6 +374,16 @@
} }
} }
}, },
"1 Kontakt wurde nicht hinzugefügt. Im Free-Tier kannst du beim Onboarding bis zu 3 Personen auswählen." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "1 contact was not added. In the Free Tier, you can select up to 3 people during onboarding."
}
}
}
},
"1 Monat" : { "1 Monat" : {
"comment" : "Settings look-ahead / period picker option", "comment" : "Settings look-ahead / period picker option",
"localizations" : { "localizations" : {
@@ -1821,6 +1841,7 @@
} }
}, },
"Du kannst Kontakte jederzeit später in der App hinzufügen." : { "Du kannst Kontakte jederzeit später in der App hinzufügen." : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -2533,6 +2554,16 @@
} }
} }
}, },
"Füge mindestens eine Person hinzu, um fortzufahren sonst macht nahbar leider keinen Sinn." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add at least one person to continue otherwise nahbar unfortunately doesn't make sense."
}
}
}
},
"Füge Personen hinzu, die dir wichtig sind. Notiere Interessen, Gesprächsthemen und was euch verbindet." : { "Füge Personen hinzu, die dir wichtig sind. Notiere Interessen, Gesprächsthemen und was euch verbindet." : {
"comment" : "TourCatalog onboarding step 2 body", "comment" : "TourCatalog onboarding step 2 body",
"localizations" : { "localizations" : {
@@ -3515,6 +3546,7 @@
} }
}, },
"Kontakte überspringen" : { "Kontakte überspringen" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -3525,6 +3557,7 @@
} }
}, },
"Kontakte überspringen?" : { "Kontakte überspringen?" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -5754,6 +5787,7 @@
} }
}, },
"Trotzdem überspringen" : { "Trotzdem überspringen" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -6679,6 +6713,7 @@
} }
}, },
"Zeigt eine Bestätigungsabfrage." : { "Zeigt eine Bestätigungsabfrage." : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
+29 -20
View File
@@ -31,8 +31,7 @@ struct OnboardingContainerView: View {
OnboardingContactImportView( OnboardingContactImportView(
coordinator: coordinator, coordinator: coordinator,
onContinue: startPrivacyScreen, onContinue: startPrivacyScreen
onSkip: startPrivacyScreen
) )
.tag(1) .tag(1)
} }
@@ -345,10 +344,10 @@ private struct OnboardingProfileView: View {
private struct OnboardingContactImportView: View { private struct OnboardingContactImportView: View {
@ObservedObject var coordinator: OnboardingCoordinator @ObservedObject var coordinator: OnboardingCoordinator
let onContinue: () -> Void let onContinue: () -> Void
let onSkip: () -> Void
@State private var showingPicker = false @State private var showingPicker = false
@State private var showSkipConfirmation: Bool = false @State private var showingLimitAlert = false
@State private var droppedByLimit = 0
private let maxContacts = 3 private let maxContacts = 3
private var atLimit: Bool { coordinator.selectedContacts.count >= maxContacts } private var atLimit: Bool { coordinator.selectedContacts.count >= maxContacts }
@@ -407,29 +406,21 @@ private struct OnboardingContactImportView: View {
: "\(coordinator.selectedContacts.count) Kontakte ausgewählt. Weiter." : "\(coordinator.selectedContacts.count) Kontakte ausgewählt. Weiter."
) )
Button { if coordinator.selectedContacts.isEmpty {
showSkipConfirmation = true Text("Füge mindestens eine Person hinzu, um fortzufahren sonst macht nahbar leider keinen Sinn.")
} label: { .font(.caption)
Text("Überspringen")
.font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center)
} }
.accessibilityLabel("Kontakte überspringen")
.accessibilityHint("Zeigt eine Bestätigungsabfrage.")
} }
.padding(.horizontal, 24) .padding(.horizontal, 24)
.padding(.vertical, 16) .padding(.vertical, 16)
} }
.confirmationDialog( .alert("Limit erreicht", isPresented: $showingLimitAlert) {
"Kontakte überspringen?", Button("OK", role: .cancel) {}
isPresented: $showSkipConfirmation,
titleVisibility: .visible
) {
Button("Trotzdem überspringen", role: .destructive, action: onSkip)
Button("Abbrechen", role: .cancel) {}
} message: { } message: {
Text("Du kannst Kontakte jederzeit später in der App hinzufügen.") Text(limitAlertMessage)
} }
.overlay(alignment: .center) { .overlay(alignment: .center) {
// Invisible trigger finds the hosting UIViewController via // Invisible trigger finds the hosting UIViewController via
@@ -519,16 +510,34 @@ private struct OnboardingContactImportView: View {
// MARK: Merge helper // MARK: Merge helper
private var limitAlertMessage: String {
if droppedByLimit == 1 {
return String(localized: "1 Kontakt wurde nicht hinzugefügt. Im Free-Tier kannst du beim Onboarding bis zu 3 Personen auswählen.")
}
return String.localizedStringWithFormat(
String(localized: "%lld Kontakte wurden nicht hinzugefügt. Im Free-Tier kannst du beim Onboarding bis zu 3 Personen auswählen."),
droppedByLimit
)
}
/// Merges newly picked contacts into the existing selection (no duplicates). /// Merges newly picked contacts into the existing selection (no duplicates).
private func mergeContacts(_ contacts: [CNContact]) { private func mergeContacts(_ contacts: [CNContact]) {
var dropped = 0
for contact in contacts { for contact in contacts {
guard coordinator.selectedContacts.count < maxContacts else { break } if coordinator.selectedContacts.count >= maxContacts {
dropped += 1
continue
}
let alreadySelected = coordinator.selectedContacts let alreadySelected = coordinator.selectedContacts
.contains { $0.cnIdentifier == contact.identifier } .contains { $0.cnIdentifier == contact.identifier }
if !alreadySelected { if !alreadySelected {
coordinator.selectedContacts.append(NahbarContact(from: contact)) coordinator.selectedContacts.append(NahbarContact(from: contact))
} }
} }
if dropped > 0 {
droppedByLimit = dropped
showingLimitAlert = true
}
} }
} }