diff --git a/PRIVACY.md b/PRIVACY.md
index e89a480b..508ca0a0 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -1,6 +1,6 @@
# Loopky Privacy Policy
-**Last updated:** 23 August 2026
+**Last updated:** 30 August 2026
Loopky is a flashcards app built on [Pubky](https://pubky.org). This policy describes what happens
to your information when you use it.
@@ -107,7 +107,7 @@ has its own operator and its own privacy practices.
| **Pubky Nexus indexer** (`nexus.pubky.app`) | Search, discovery, trending topics, profile pictures | Your search terms, the public keys and decks you look up, your IP address |
| **Homegate** (`homegate.pubky.app`) | Signing up for a new homeserver account only | Your phone number if you verify by SMS, or Lightning payment details if you pay. Your phone number is handled by Homegate and is never stored by Loopky |
| **Unsplash** (`api.unsplash.com`) | Only when you search for a picture from the web | Your search terms and your IP address, under [Unsplash's privacy policy](https://unsplash.com/privacy) |
-| **Google Play** | Only if you tap to install Pubky Ring from the backup screen | Standard Play Store request |
+| **Google Play** or the **App Store** | Only if you tap to install Pubky Ring from the backup screen | A standard store request, to whichever store your device uses |
Loopky does not contact any of these to report on you. It contacts them to do the thing you asked
for.
@@ -125,16 +125,24 @@ The **Speak** practice mode records audio so you can practise pronunciation.
- The permission is requested the first time you use Speak, and Speak is optional. Declining it
leaves the rest of the app working normally.
-Depending on your device settings, your device's speech recognition may itself run in the cloud
-rather than on-device. That processing is governed by your device manufacturer's or Google's
-privacy policy, not this one.
+Depending on your device and its settings, your device's speech recognition may itself run in the
+cloud rather than on-device — on iOS through Apple's speech recognition, on Android through
+Google's. Loopky does not force either mode, so which one you get is your platform's decision. That
+processing is governed by Apple's or Google's privacy policy, not this one.
---
-## 7. Photos and images
+## 7. Camera, photos and images
-Choosing a picture for a card uses the Android system photo picker, which hands Loopky only the one
-image you select. Loopky requests no storage or gallery permission.
+**The camera is used for one thing: reading a QR code that holds someone's pubky**, so you can add
+them without typing a long key by hand. Loopky reads only the text decoded from the code. No photo
+or video is captured, saved or uploaded, and the camera runs only while that scanner is open. The
+permission is requested the first time you open it, and scanning is optional — you can always type
+or paste a pubky instead.
+
+Choosing a picture for a card uses the system photo picker — `PhotosPicker` on iOS, the Android
+photo picker on Android. Both run outside Loopky and hand it only the one image you select, so
+Loopky requests no gallery or storage permission on either platform.
Images you attach to a published deck are uploaded to your homeserver and are public along with the
deck.
diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig
index 1ea244ba..275cb449 100644
--- a/iosApp/Configuration/Config.xcconfig
+++ b/iosApp/Configuration/Config.xcconfig
@@ -1,7 +1,29 @@
+// Your Apple Developer Team ID, from developer.apple.com → Membership.
+// Ten characters, e.g. A1B2C3D4E5. Needed to sign a device build or archive;
+// a simulator build works without it.
TEAM_ID=
+// The app's permanent identity on the App Store.
+//
+// This is deliberately NOT derived from TEAM_ID. The Kotlin Multiplatform template
+// ships it as `...Loopky$(TEAM_ID)`, which keeps two developers' dev builds from
+// colliding but makes the bundle identifier change the moment TEAM_ID is filled in —
+// silently breaking the match with the App Store Connect record and every provisioning
+// profile. It is fixed here instead.
+//
+// Change it only BEFORE the app record exists in App Store Connect. After that it is
+// permanent: a new identifier is a new app, with no path to move testers or reviews.
+//
+// Matches Android's applicationId exactly. It also makes the identifiers already in
+// Info.plist real children of it rather than merely looking like them: the two
+// BGTaskScheduler ids, the `...loopky.login` URL name, and the `.apkg` / `.pkarr` UTIs
+// were all written as `com.github.jvsena42.loopky.*` while the bundle was
+// `...loopky.Loopky`, so none of them actually nested under it.
+PRODUCT_BUNDLE_IDENTIFIER=com.github.jvsena42.loopky
+
PRODUCT_NAME=Loopky
-PRODUCT_BUNDLE_IDENTIFIER=com.github.jvsena42.loopky.Loopky$(TEAM_ID)
+// MARKETING_VERSION is what testers see. CURRENT_PROJECT_VERSION must be unique for
+// every upload to App Store Connect — bump it per upload, not per release.
CURRENT_PROJECT_VERSION=13
-MARKETING_VERSION=0.6.1
\ No newline at end of file
+MARKETING_VERSION=0.6.1
diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist
index ce8b66df..0be7ecc6 100644
--- a/iosApp/iosApp/Info.plist
+++ b/iosApp/iosApp/Info.plist
@@ -2,8 +2,28 @@
+ BGTaskSchedulerPermittedIdentifiers
+
+ com.github.jvsena42.loopky.media-rehost
+ com.github.jvsena42.loopky.deck-compaction
+
CADisableMinimumFrameDurationOnPhone
+ CFBundleDocumentTypes
+
+
+ CFBundleTypeName
+ Anki deck
+ LSHandlerRank
+ Alternate
+ LSItemContentTypes
+
+ com.github.jvsena42.loopky.apkg
+ public.plain-text
+ public.comma-separated-values-text
+
+
+
CFBundleURLTypes
@@ -15,60 +35,38 @@
-
-
-
+ ITSAppUsesNonExemptEncryption
+
+ LSApplicationQueriesSchemes
+
+ pubkyring
+ pubkyauth
+
+ LSSupportsOpeningDocumentsInPlace
+
NSCameraUsageDescription
Loopky uses the camera to read a QR code holding someone's pubky, so you can find them without typing it.
NSMicrophoneUsageDescription
Loopky listens while you practise saying a card out loud, and stops as soon as you have answered.
NSSpeechRecognitionUsageDescription
Loopky turns what you say into text to check your pronunciation against the card. Nothing is stored.
- CFBundleDocumentTypes
+ UIBackgroundModes
-
- CFBundleTypeName
- Anki deck
- LSHandlerRank
- Alternate
- LSItemContentTypes
-
- com.github.jvsena42.loopky.apkg
- public.plain-text
- public.comma-separated-values-text
-
-
+ processing
-
+ UIFileSharingEnabled
+
UTExportedTypeDeclarations
- UTTypeIdentifier
- com.github.jvsena42.loopky.pkarr
- UTTypeDescription
- Pubky recovery file
UTTypeConformsTo
public.data
+ UTTypeDescription
+ Pubky recovery file
+ UTTypeIdentifier
+ com.github.jvsena42.loopky.pkarr
UTTypeTagSpecification
public.filename-extension
@@ -81,14 +79,14 @@
UTImportedTypeDeclarations
- UTTypeIdentifier
- com.github.jvsena42.loopky.apkg
- UTTypeDescription
- Anki deck
UTTypeConformsTo
public.zip-archive
+ UTTypeDescription
+ Anki deck
+ UTTypeIdentifier
+ com.github.jvsena42.loopky.apkg
UTTypeTagSpecification
public.filename-extension
@@ -98,25 +96,5 @@
- UIFileSharingEnabled
-
- LSSupportsOpeningDocumentsInPlace
-
- BGTaskSchedulerPermittedIdentifiers
-
- com.github.jvsena42.loopky.media-rehost
- com.github.jvsena42.loopky.deck-compaction
-
- LSApplicationQueriesSchemes
-
- pubkyring
-
- pubkyauth
-
- UIBackgroundModes
-
- processing
-
diff --git a/iosApp/iosApp/PrivacyInfo.xcprivacy b/iosApp/iosApp/PrivacyInfo.xcprivacy
new file mode 100644
index 00000000..4ace3a4f
--- /dev/null
+++ b/iosApp/iosApp/PrivacyInfo.xcprivacy
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+ NSPrivacyTracking
+
+ NSPrivacyTrackingDomains
+
+
+
+ NSPrivacyCollectedDataTypes
+
+
+
+ NSPrivacyAccessedAPITypes
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryUserDefaults
+ NSPrivacyAccessedAPITypeReasons
+
+ CA92.1
+
+
+
+
+