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
9 changes: 9 additions & 0 deletions PROJECT_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ multiplayer are **v2**, already scaffolded and parked in-repo.

## Timeline

- **2026-08-11** — Prepared the iPhone/iPad beta for TestFlight transport with
versioned App Store metadata, a privacy manifest and public privacy surface,
clean simulator screenshot evidence, an archive-safe bundled web-game sync,
and a personal-team signed archive. App Store Connect record creation and
physical camera/ReplayKit verification remain separate manual gates.
- **2026-08-09** — Pinned the landing release tool in the workspace lockfile
and switched the deploy script to `pnpm exec`, removing npm/pnpm configuration
deprecation warnings from the manual Cloudflare Pages release path.
Expand Down Expand Up @@ -74,6 +79,10 @@ multiplayer are **v2**, already scaffolded and parked in-repo.

## Features (shipped)

- **iOS beta preparation** — App Store identity/copy, Apple-accepted iPhone and
iPad simulator screenshots, privacy metadata and support links, stable
version/build settings, a clean default game canvas, and reproducible bundled
web assets are ready for the existing manual TestFlight boundary.
- **Public landing** — responsive static product story with the existing Motion
logo, canonical/share metadata, `llms.txt`, `index.md`, `/api/ai`, and an
owned `/changelog` source with semantic dates and internal discovery.
Expand Down
Binary file added artifacts/app-store/ipad-13/motion-maker-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
87 changes: 87 additions & 0 deletions docs/app-store/APP_STORE_SUBMISSION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Motion App Store submission draft

Preparation only. Do not create, upload, publish, or submit an App Store Connect record from this repository.

## Identity

- Name: Motion
- Bundle ID: `com.motion.controller`
- SKU: `motion-ios-1`
- Version: `1.0.0`
- Build: `1`
- Primary language: English (U.S.)
- Primary category: Games
- Secondary category: Health & Fitness
- Copyright: `2026 Sarthak Agrawal`
- License: Apple's standard EULA
- Content rights: Motion owns or is licensed to use the bundled game, artwork, model, and audio-visual assets.

## Store copy

**Subtitle**
Move your body. Play.

**Promotional text**
Turn your iPhone camera into a private, on-device motion controller. Move, grab, dodge, and record the play you make.

**Description**
Your body is the controller.

Motion turns movement into play using your iPhone camera and on-device pose detection. Step into frame, follow the live framing guide, and start Motion Maker—a tactile playground where your hands grab, carry, and throw objects.

The complete primary game runs on your iPhone with no account and no server required. Mirror your screen to a larger display when you want the room to become the play space.

• Camera processing stays on device
• Motion Maker is bundled and works without a Mac
• Portrait full-body and landscape upper-body modes
• Front and wide-rear camera support
• Clap-to-start when you are standing away from the phone
• Optional ReplayKit recording saved directly to Photos
• Optional browser display for live normalized movement—never camera frames

Motion is an experimental movement game, not a fitness or medical service. Use it in a clear space and move within your own comfort and ability.

**Keywords**
movement,body,controller,game,pose,camera,play,motion,active,airplay

## URLs

- Support: `https://motion.significanthobbies.com`
- Marketing: `https://motion.significanthobbies.com`
- Privacy: `https://motion.significanthobbies.com/privacy`

The privacy source is prepared at `landing/privacy.html`; the live route must return HTTP 200 before submission.

## App privacy answers

- Tracking: No
- Data collected: No
- Camera frames and pose inference: processed on device
- Optional relay: transient normalized movement and room messages are forwarded to service a live session and are not retained
- Advertising, attribution, and third-party analytics: none
- IDFA: not used

## Age rating draft

- Made for Kids: No
- Gambling, contests, simulated gambling, loot boxes: None
- Sexual content, profanity, horror, drugs, alcohol, tobacco: None
- Medical or wellness topics: None
- User-generated content, messaging, unrestricted web access: None
- Cartoon/fantasy violence: Infrequent/Mild, because Motion Maker includes stylized sword-like play objects

Confirm the rating produced by App Store Connect's current questionnaire; do not manually claim a lower rating.

## Review notes

No login is required. Grant Camera access and stand where the on-screen framing guide can see the requested body area. The primary Motion Maker game is bundled in the app and does not need a server. Rotate to portrait for full-body controls or landscape for upper-body controls. Recording is optional and requests Photos add-only permission when used. The “Stream to website” setting is optional and is not required to review the primary experience.

Physical camera, pose accuracy, rotation, mirroring, ReplayKit output, and Photos saving require a real iPhone and remain device-verification items.

## Screenshots

- iPhone 6.9-inch portrait: `artifacts/app-store/iphone-6.9/motion-maker-clean.png` (`1206 × 2622`)
- iPad 13-inch portrait: `artifacts/app-store/ipad-13/motion-maker-clean.png` (`2064 × 2752`)
- Both files use Apple-accepted dimensions and were captured from the Release simulator build with the camera-free synthetic-pose harness
- App previews: omit for version 1.0
- Release: manual
23 changes: 23 additions & 0 deletions ios/Resources/PrivacyInfo.xcprivacy
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
</array>
</dict>
</plist>
4 changes: 2 additions & 2 deletions ios/Resources/webgame/index.html

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions ios/Sources/Motion/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
Expand Down
10 changes: 10 additions & 0 deletions ios/Sources/Motion/Views/SetupView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,16 @@ struct SetupView: View {
streamStatusPill
}

Divider().overlay(.white.opacity(0.2))

HStack(spacing: 20) {
Link("Privacy", destination: URL(string: "https://motion.significanthobbies.com/privacy")!)
Link("Support", destination: URL(string: "https://motion.significanthobbies.com")!)
}
.font(.caption.bold())
.foregroundStyle(.white)
.frame(maxWidth: .infinity, alignment: .leading)

// Live hand-openness debug so the user can confirm open/close is detected.
if let hands = model.hands {
Text(String(format: "Hands L %.2f R %.2f", hands.left, hands.right))
Expand Down
6 changes: 6 additions & 0 deletions ios/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,20 @@ targets:
# phase). See Vision/HandLandmarkerEstimator.swift for how it's loaded.
- path: Resources/mediapipe/hand_landmarker.task
buildPhase: resources
- path: Resources/PrivacyInfo.xcprivacy
buildPhase: resources
info:
# XcodeGen GENERATES this plist from the properties below on every `xcodegen
# generate`, so the keys MUST live here (a hand-edited Info.plist would be
# overwritten). Missing usage strings => hard TCC crash on first camera access.
path: Sources/Motion/Info.plist
properties:
CFBundleShortVersionString: "$(MARKETING_VERSION)"
CFBundleVersion: "$(CURRENT_PROJECT_VERSION)"
NSCameraUsageDescription: "Motion uses the front camera on-device to turn your body movements into game controls. Camera video never leaves your phone."
NSPhotoLibraryAddUsageDescription: "Motion saves your gameplay recordings (the game plus your camera) to Photos."
NSLocalNetworkUsageDescription: "Motion connects to the game running on your computer over your local Wi-Fi."
ITSAppUsesNonExemptEncryption: false
NSAppTransportSecurity:
NSAllowsLocalNetworking: true
UILaunchScreen: {}
Expand All @@ -71,6 +76,7 @@ targets:
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.motion.controller
INFOPLIST_KEY_LSApplicationCategoryType: public.app-category.games
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
GENERATE_INFOPLIST_FILE: NO
TARGETED_DEVICE_FAMILY: "1,2" # iPhone + iPad
Expand Down
138 changes: 138 additions & 0 deletions landing/privacy.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Privacy — Motion</title>
<meta
name="description"
content="How Motion handles camera, movement, recordings, and optional local-network play."
/>
<link rel="canonical" href="https://motion.significanthobbies.com/privacy" />
<link rel="icon" type="image/png" href="/assets/motion-logo.png" />
<meta name="theme-color" content="#05070d" />
<link rel="stylesheet" href="/styles.css" />
</head>
<body class="changelog-page">
<a class="skip-link" href="#main">Skip to content</a>

<header class="site-header">
<a class="brand" href="/" aria-label="Motion home">
<img src="/assets/motion-logo.png" alt="" width="48" height="48" />
<span>Motion</span>
</a>
<a class="portfolio-link" href="https://significanthobbies.com/">
A Significant Hobby
<span aria-hidden="true">↗</span>
</a>
</header>

<main id="main" class="changelog-main">
<section class="changelog-intro" aria-labelledby="privacy-title">
<div>
<p class="status"><span></span> Privacy by architecture</p>
<h1 id="privacy-title">The camera stays with you.</h1>
</div>
<div class="changelog-lede">
<p>
Motion turns camera frames into movement controls on your iPhone.
Camera frames are not transmitted to us or stored by us.
</p>
<p class="changelog-boundary">Last updated 11 August 2026.</p>
</div>
</section>

<section class="release-trail" aria-label="Motion privacy details">
<ol>
<li>
<div></div>
<div>
<p class="release-kicker">On device</p>
<h2>Camera and movement</h2>
<p>
The app uses the camera to estimate body and hand positions on
device. In normal single-device play, those positions stay
inside the app and drive the bundled game directly.
</p>
</div>
</li>
<li>
<div></div>
<div>
<p class="release-kicker">Your choice</p>
<h2>Recordings and Photos</h2>
<p>
Motion records only when you start recording. The completed
gameplay clip is saved to your Photos library and temporary
working files are removed after use. Motion does not upload or
retain your recording.
</p>
</div>
</li>
<li>
<div></div>
<div>
<p class="release-kicker">Optional</p>
<h2>Laptop and browser play</h2>
<p>
If you explicitly enable “Stream to website,” Motion sends
normalized movement coordinates and a room code to the browser
session you choose. Camera frames are never included. The relay
forwards live session messages and does not persist movement or
gameplay content after the session.
</p>
</div>
</li>
<li>
<div></div>
<div>
<p class="release-kicker">No profiles</p>
<h2>Accounts, ads, and tracking</h2>
<p>
Motion has no account, advertising SDK, third-party analytics,
or cross-app tracking. We do not sell personal information.
Service providers may process ordinary network connection data
when you use an online relay, subject to their security and
privacy safeguards.
</p>
</div>
</li>
<li>
<div></div>
<div>
<p class="release-kicker">Control</p>
<h2>Retention, deletion, and consent</h2>
<p>
You can revoke Camera, Photos, and Local Network access in iOS
Settings. Delete saved clips in Photos. Because Motion does not
keep an account or server-side profile, there is no Motion
account data to request or delete.
</p>
</div>
</li>
</ol>
</section>

<section class="changelog-return" aria-labelledby="contact-title">
<p class="section-label">Questions</p>
<h2 id="contact-title">Privacy should be legible.</h2>
<a class="secondary-action" href="https://github.com/Significant-Hobbies/motion/issues">
Contact support
<span aria-hidden="true">↗</span>
</a>
</section>
</main>

<footer>
<div>
<img src="/assets/motion-logo.png" alt="" width="64" height="64" />
<p>Make room.<br /><strong>The controller is already here.</strong></p>
</div>
<nav class="footer-links" aria-label="Footer">
<a href="/">Motion home</a>
<a href="/changelog">Changelog</a>
<a href="https://github.com/Significant-Hobbies/motion/issues">Support</a>
</nav>
</footer>
</body>
</html>
3 changes: 3 additions & 0 deletions landing/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@
<url>
<loc>https://motion.significanthobbies.com/changelog</loc>
</url>
<url>
<loc>https://motion.significanthobbies.com/privacy</loc>
</url>
</urlset>
8 changes: 7 additions & 1 deletion scripts/check-landing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const landing = path.join(root, "landing");
const required = [
"index.html",
"changelog.html",
"privacy.html",
"404.html",
"styles.css",
"llms.txt",
Expand All @@ -27,6 +28,7 @@ await Promise.all(required.map((file) => stat(path.join(landing, file))));
const [
html,
changelog,
privacy,
css,
llms,
markdown,
Expand All @@ -39,6 +41,7 @@ const [
await Promise.all([
readFile(path.join(landing, "index.html"), "utf8"),
readFile(path.join(landing, "changelog.html"), "utf8"),
readFile(path.join(landing, "privacy.html"), "utf8"),
readFile(path.join(landing, "styles.css"), "utf8"),
readFile(path.join(landing, "llms.txt"), "utf8"),
readFile(path.join(landing, "index.md"), "utf8"),
Expand Down Expand Up @@ -67,8 +70,11 @@ assert.equal((changelog.match(/<time datetime="\d{4}-\d{2}-\d{2}">/g) ?? []).len
assert.match(changelog, /https:\/\/github\.com\/Significant-Hobbies\/motion\/issues/);
assert.match(changelog, /https:\/\/github\.com\/Significant-Hobbies\/motion/);
assert.match(sitemap, /https:\/\/motion\.significanthobbies\.com\/changelog/);
assert.match(privacy, /Camera frames are not transmitted to us or stored by us/);
assert.match(privacy, /href="https:\/\/github\.com\/Significant-Hobbies\/motion\/issues"/);
assert.match(sitemap, /https:\/\/motion\.significanthobbies\.com\/privacy/);

for (const text of [html, changelog, llms, markdown, JSON.stringify(ai)]) {
for (const text of [html, changelog, privacy, llms, markdown, JSON.stringify(ai)]) {
assert.doesNotMatch(text, /href=["']\/play|debug=1|camera=1|room=|127\.0\.0\.1|localhost/i);
}

Expand Down
7 changes: 2 additions & 5 deletions scripts/sync-webgame.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,10 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DEST="$ROOT/ios/Resources/webgame"

echo "→ building web game…"
npm --workspace web run build
pnpm --filter motion-web run build

echo "→ syncing dist → $DEST"
rm -rf "$DEST"
mkdir -p "$DEST"
cp -R "$ROOT/web/dist/." "$DEST/"
# Sourcemaps aren't needed in the shipped app — drop them to keep the bundle lean.
find "$DEST" -name '*.map' -delete
rsync -a --exclude '*.map' "$ROOT/web/dist/" "$DEST/"

echo "✓ webgame bundled ($(find "$DEST" -type f | wc -l | tr -d ' ') files). Rebuild the iOS app to pick it up."
Loading
Loading