App Store listing copy, and screenshots the store will accept

`Scripts/screenshots-ios.sh` rendered at 1125x2778... 1125x2436, the
375x812 design canvas at 3x. App Store Connect takes 1242x2688,
2688x1242, 1284x2778 or 2778x1284 and nothing else, so every shot was
rejected on size. The aspect ratios are close enough that rescaling
looks fine and is still not one of those numbers.

`FNMSG_SHOT_SIZE=1284x2778` now asks for a pixel size. Points are
derived from it at a pinned 3x, so the layout gets the room a device
that size really has rather than a scaled-up picture of a smaller one,
and the same request gives the same pixels on any simulator. Unset, the
design canvas is unchanged, which is what it is for.

⚠️ Getting the value into the test took three attempts, recorded in the
script so the next person skips them: `FOO=bar` on an xcodebuild line is
a build setting the test never sees, and the TEST_RUNNER_ prefix only
reaches a UI test's runner, not unit tests hosted in the app. Both
arrived unset. `simctl spawn launchctl setenv` works, and the script
unsets it when no size is asked for, or a previous sized run would
silently apply to the next one.

STORE-LISTING.md carries the rest of the App Store tab: promotional
text 149/170, description 2358/4000, keywords 97/100, support and
marketing URLs, copyright, and why the routing coverage file stays
empty. Every URL in it returns 200. It names no competitor, and says
plainly that the app has not been audited.

764 XCTest and 75 Swift Testing pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-09 08:43:21 -04:00
co-authored by Claude Opus 5
parent 7760ece24f
commit 8051d38009
3 changed files with 197 additions and 4 deletions
+147
View File
@@ -0,0 +1,147 @@
# App Store Connect listing
Copy for the App Store tab. TestFlight's own fields are in `TESTFLIGHT.md`;
these are the ones the public listing uses.
Every claim here is one the project can show. Nothing says audited, nothing
says unbreakable, and nothing compares this to another messenger by name. See
the positioning note in `SECURITY.md`: what survives scrutiny is "a private
messenger you host yourself, with no phone number", not a superlative.
## Promotional Text
Editable without a new build, so it is the field to use for anything that
changes. 170 characters; this is 146.
```
Now in open beta. Messages, files and a wallet in one app, encrypted on your
device. Sign up with a username. Run the server yourself if you want to.
```
## Description
4000 characters allowed; this is about 2,400.
```
Flamenet Messenger is a private messenger you can host yourself.
Sign up with a username. No phone number, no email required, no contact upload.
Your address is derived from your passphrase rather than issued by a server, so
signing in on a different server keeps the same identity.
WHAT IS ENCRYPTED
Every one-to-one and group message is end-to-end encrypted on your device using
the X3DH and Double Ratchet protocols. Photos, voice notes, files and stories
get the same treatment with a per-file key. There is no plain mode to fall back to: if the app cannot encrypt, it
refuses to send and tells you why.
Newer conversations add a post-quantum key agreement (ML-KEM-768) alongside the
classical one, so a conversation recorded today is not readable later by a
computer that does not exist yet. It holds if either half holds.
WHO YOU ARE TALKING TO
Every conversation has a safety number you can compare in person or over
another channel. If someone's keys change, because they reinstalled, added a
device, or something worse, the app says so and holds the message until you
decide. Your app also checks, on its own, that the key a server hands you is
one that server has published to a log it cannot quietly rewrite.
WHAT THE SERVER SEES
The relay carries sealed envelopes. It does not hold the keys that open them.
It does see who a message is addressed to, when it arrived, and how large it
is, and that is written out plainly at flamenetmessenger.com/metadata rather
than left for you to assume. Photos have their location and camera data
stripped before they are sent.
FILES AND WALLET
Files keeps documents encrypted on your device, with a classification you
choose, and sends them sealed. Wallet makes and keeps keys from a standard
twenty-four-word phrase; it is not connected to any network, and it says so
rather than showing you a balance it cannot know.
HOST IT YOURSELF
The relay is open source and published, not described. Two containers and a
config file. An account on your own server is a real account, not a limited
one, and servers can exchange messages with each other if you allow it.
WHAT THIS IS NOT
This app has not been independently audited. It is built in the open by a small
team and the source is there to read. Read flamenetmessenger.com/security
before you rely on it for anything where being wrong would hurt.
```
## Keywords
100 characters including commas; this is 98. No competitor names: Apple
rejects those, and they would be the comparison the positioning note refuses.
```
encrypted,messenger,private,chat,self-hosted,no phone number,secure,files,e2ee,open source,wallet
```
## Support URL
```
https://flamenetmessenger.com/community
```
## Marketing URL
```
https://flamenetmessenger.com
```
## Copyright
The legal name has no comma; `Extant 2000, LLC` is wrong.
```
2026 Extant 2000 LLC
```
## Routing App Coverage File
Leave empty. It is only for apps that give point-to-point routing directions,
and uploading one would claim a capability this app does not have.
## Availability
United States, Mexico, Guatemala, El Salvador, Honduras, Peru, Bolivia. The
same list the relay's sign-up geogate enforces, so the listing and the server
agree about where an account can be created.
## Screenshots
Up to 10, plus up to 3 app previews. Accepted sizes are 1242 x 2688 or
2688 x 1242 (6.5 inch), and 1284 x 2778 or 2778 x 1284 (6.7 inch).
`Scripts/screenshots-ios.sh` renders them. Ask for the size; unset, it renders
the 375 x 812 design canvas, which the store does not accept:
```bash
FNMSG_SHOT_SIZE=1284x2778 Scripts/screenshots-ios.sh
```
The size is in pixels and the points are derived from it at 3x, so the layout
gets the room a device that size really has rather than a scaled-up picture of
a smaller one. `1242x2688` works the same way. The script prints the pixel size
of every file it writes, so a wrong one is visible without opening Preview.
Suggested order, most convincing first, one idea per shot:
1. A conversation, showing a sent and received message.
2. Sign-up, showing that it asks for a username and not a phone number.
3. The safety number screen.
4. A key-change notice, holding a message until you decide.
5. Files, with a classification on a document.
6. Wallet, with the phrase screen.
7. Stories.
8. A call in progress.
9. Settings, showing the privacy controls.
10. The self-hosting screen, or a second server in the mailbox list.
+26 -2
View File
@@ -17,6 +17,17 @@ DEVICE_NAME="fnmsg-shots"
RUNTIME="${FNMSG_RUNTIME:-com.apple.CoreSimulator.SimRuntime.iOS-26-5}"
TYPE="${FNMSG_DEVICE:-com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro}"
OUT="build/screenshots-ios"
# App Store Connect takes 1242x2688, 2688x1242, 1284x2778 or 2778x1284 and
# nothing else. Unset, the tests render the 375x812 design canvas, which is
# right for looking at a layout and is not a size the store will accept.
# FNMSG_SHOT_SIZE=1284x2778 Scripts/screenshots-ios.sh
#
# Handed over with `simctl spawn launchctl setenv`, and neither of the two
# obvious routes works: `FOO=bar` on an xcodebuild line is a BUILD SETTING the
# test process never sees, and the TEST_RUNNER_ prefix only reaches a UI test's
# runner, not unit tests hosted in the app. Both were tried; both arrived
# unset.
SHOT_SIZE="${FNMSG_SHOT_SIZE:-}"
udid=$(xcrun simctl list devices -j | python3 -c "
import json,sys
@@ -35,7 +46,14 @@ fi
echo "==> device $udid"
xcrun simctl boot "$udid" >/dev/null 2>&1 || true
echo "==> rendering"
if [ -n "$SHOT_SIZE" ]; then
xcrun simctl spawn "$udid" launchctl setenv FNMSG_SHOT_SIZE "$SHOT_SIZE"
echo "==> rendering at $SHOT_SIZE"
else
# Left over from a previous sized run, it would silently apply to this one.
xcrun simctl spawn "$udid" launchctl unsetenv FNMSG_SHOT_SIZE 2>/dev/null || true
echo "==> rendering"
fi
log=$(mktemp)
xcodebuild test \
-project FlamenetMessenger.xcodeproj \
@@ -51,4 +69,10 @@ grep '^SHOT ' "$log" | sed 's/^SHOT //' | while read -r f; do
[ -f "$f" ] && cp "$f" "$OUT/"
done
echo "==> wrote:"
ls -1 "$OUT"/*.png 2>/dev/null | sed 's/^/ /' || echo " (nothing, see $log)"
# The size is the whole point when these are going to the store, so print it
# rather than leave somebody to check in Preview.
for f in "$OUT"/*.png; do
[ -f "$f" ] || { echo " (nothing, see $log)"; break; }
printf ' %s %s\n' "$(sips -g pixelWidth -g pixelHeight "$f" 2>/dev/null |
awk '/pixelWidth/{w=$2} /pixelHeight/{h=$2} END{printf "%sx%s", w, h}')" "$f"
done
+24 -2
View File
@@ -10,7 +10,27 @@ final class ScreenshotTests: XCTestCase {
/// The kit's canvas, so a PNG can be put beside the Sketch artboard
/// directly rather than mentally rescaled from whatever device ran it.
private let canvas = CGSize(width: 375, height: 812)
///
/// `FNMSG_SHOT_SIZE=<w>x<h>` asks for a pixel size instead, which is how
/// the App Store's accepted sizes are produced: it only takes 1242x2688,
/// 2688x1242, 1284x2778 or 2778x1284, and the design canvas is none of
/// them. Points are the pixels divided by the scale below, so the layout
/// gets the room a real device of that size has rather than a scaled-up
/// picture of a smaller one.
private var canvas: CGSize {
guard let raw = ProcessInfo.processInfo.environment["FNMSG_SHOT_SIZE"],
let x = raw.firstIndex(of: "x"),
let w = Double(raw[raw.startIndex..<x]),
let h = Double(raw[raw.index(after: x)...]),
w > 0, h > 0
else { return CGSize(width: 375, height: 812) }
return CGSize(width: w / scale, height: h / scale)
}
/// Fixed rather than the running simulator's, so the same request gives
/// the same pixels on any device. Every size the App Store accepts is a
/// 3x one.
private let scale: CGFloat = 3
private lazy var outputDirectory: URL = {
let base = FileManager.default.temporaryDirectory
@@ -239,7 +259,9 @@ final class ScreenshotTests: XCTestCase {
// One runloop turn.
RunLoop.current.run(until: Date().addingTimeInterval(0.35))
let renderer = UIGraphicsImageRenderer(size: canvas)
let format = UIGraphicsImageRendererFormat()
format.scale = scale
let renderer = UIGraphicsImageRenderer(size: canvas, format: format)
let image = renderer.image { _ in
host.view.drawHierarchy(in: host.view.bounds, afterScreenUpdates: true)
}