Skip to content

Pre-launch Checklist ​

Status: Active

Detailed checklist for everything that must complete BEFORE the app is publicly listed on App Store / Play Store. Use External Blockers Register for the summary routing view; this file owns the detailed actions, code/data locations, URLs, and risk notes.

Status legend: ⛔ blocked on user/external · ⏳ in progress · ✅ done · ➖ optional

All items in this document are PARKED. They unblock launch but don't unblock engineering work. The team is NOT actively trying to close them right now — they require external services (Apple Developer, Korean counsel, KCC lbsc.kr filing) or user-side info (CPO contact) that the engineering team cannot supply. Engineering focuses on app hardening + features per docs/production-readiness.md. When external prerequisites land, the engineering steps in each section below are minutes-to-hours of work.

Last reviewed: 2026-06-28 (canonical release-versioning and preview-first OTA workflow documented)


⛔ 1. CPO (Chief Privacy Officer) real contact info — PIPA §30 mandatory ​

Verified 2026-09-24: the placeholder is gone. The CPO block now lives in packages/app/src/config/legal/documents/privacy.ko.ts (§11, around lines 221-225 — privacy-screen.tsx is the 80-line screen component that renders this document; the CPO text itself was moved into the document module), and it already names a real person and a real, working contact channel:

'   · 성명: 이성규\n' +
'   · 직책: 대표\n' +
'   · 이메일: privacy@twomore.app\n' +
'   · 문의: privacy@twomore.app\n\n' +

No phone line is listed — the contact channel is deliberately email-only by design (see the code comment at that line), which PIPA §30(1)(7) permits (a real, monitored channel is the requirement; email-only is a valid channel choice, not a placeholder). What remains is an owner confirmation, not a designation task: confirm 이성규 is still the correct 개인정보 보호책임자 and privacy@twomore.app is a monitored mailbox before public launch.

Why: Korean Personal Information Protection Act §30(1)(7) requires every PII-collecting service to publicly designate a CPO with real name and a real contact channel — placeholder values are an immediate PIPC violation upon first user signup. The 2024–2026 enforcement trend treats placeholder CPO data as an aggravating factor in penalty calculations (KakaoPay/Apple §28-8 ruling, Jan 2025).

Action:

  1. Owner confirms 이성규 (대표) is still the right person to hold legal standing for privacy decisions (PIPA Article 31), and that privacy@twomore.app is an actively monitored mailbox.
  2. If either has changed, update the CPO block in privacy.ko.ts §11 directly (no TODO annotation to drop — there is none).
  3. Commit + push + yarn ota:preview while the app is pre-production. Production OTA is launch-gated until R4 Launch Readiness is accepted.

Risk if skipped: First user signup triggers PIPA §30 violation if the confirmation reveals the named contact is stale. Penalty escalates per user. Range: KRW 5M–30M base + per-user uplift.


⛔ 2. Counsel-approved Korean policy text — PIPA §15 / §17 / §30, 정통망법 §50 ​

Why: Current KR-1.0.0 policy text was assistant-drafted from PIPA + 정통망법 templates. It tracks the regulatory structure but has not been reviewed by Korean counsel. Counsel review catches: (a) idiomatic Korean legal-drafting conventions the LLM doesn't know, (b) hidden disclosures the LLM might miss for the project's specific data flows (cross-border to Supabase US, Sentry US, Expo Push US — all three are §28-8 entrustment cases), (c) edge cases like guardian consent for minors (PIPA §22-2) that interact with the under-14 birth-year gate.

Action:

  1. Engage a Korean privacy / IT-law counsel (typical retainer: KRW 3-5M for a startup-tier MVP policy review, ~2-3 week turnaround).
  2. Provide them: the policy documents privacy.ko.ts and terms.ko.ts (the in-app screens render these), the purpose_catalog seed migration (every purpose key + retention + legal basis), the location inventory below, and a short ops-flow doc (what data goes where).
  3. Apply edits to the documents (each carries its own version).
  4. Bump packages/app/src/config/legal/policy-versions.ts:
    ts
    terms:    { version: 'KR-1.0.1', url: ... },
    privacy:  { version: 'KR-1.0.1', url: ... },
    crossBorder: { version: 'KR-1.0.1', url: ... },
    location: { version: 'KR-1.0.1', url: ... },
    marketing: { version: 'KR-1.0.1', url: ... },
  5. Ship a new migration 001XX_policy_versions_kr_1_0_1.sql that INSERTs a new row into public.policy_versions for each affected policy type, with the new content_hash. Existing users get re-prompted to consent to the new version next time they grant any consent.
  6. Update docs/rebuild-log.md with the counsel review date + reviewer name (for audit trail).

Known corrections to bring to counsel (2026-09-24). GPS 출석 체크 was retired by migration 00384 (2026-07-11: attendance is derived from match records), but privacy.ko.ts still describes it in four places. The text is unchanged until counsel approves new copy; proposed wording:

WhereNowProposed
제1조 ③위치 기반 서비스: 코트 주변 동호인 검색, GPS 출석 체크, 지역 기반 일정 추천 (신고번호: 신고 진행 중 …)위치 기반 서비스: 가까운 코트 순 정렬과 코트·클럽까지의 거리 표시, 일정 검색의 기본 지역 설정 (기기 위치는 기기 안에서만 처리되며 회사 서버로 전송·저장되지 않습니다)
제2조 [선택]GPS 위치 정보(GPS 출석 체크 기능 이용 시, 기능 이용 후 즉시 파기)기기 위치(가까운 코트 순 정렬·기본 지역 설정 이용 시, 기기 안에서만 처리하며 서버로 전송·저장하지 않음)
제7조 ④GPS 위치 정보: 기능 이용 목적 달성 즉시 파기(출석 확인 완료 후 서버에 좌표 원본 미보관)삭제(서버에 저장하는 위치 정보가 없음), or: 기기 위치: 서버에 저장하지 않음
제8조 last paragraphGPS 위치 좌표는 출석 확인 완료 즉시 서버에서 삭제되며 원본 좌표가 보관되지 않습니다.기기 위치는 서버로 전송되지 않습니다. 업로드되는 사진은 기기에서 다시 인코딩되어 촬영 위치(EXIF GPS)가 남지 않습니다.

Location inventory (verified against code, 2026-09-24):

  • The app reads the device position in two places, each after the OS permission prompt that a user action triggers: the pickup filters' default region (use-gps-default-region.ts: one reading, mapped to a 시/도 key on the device, kept in memory for the app session) and the venue finder's 가까운 순 sort (use-venue-finder-data.ts: distances computed on the device). Club cards show a distance only if permission was already granted, never prompting (use-user-location.ts).
  • No coordinate or GPS-derived value is sent to the server or stored. The default region is never auto-saved; only a region the user picks becomes their saved filter preference.
  • Uploaded photos are re-encoded on the device before upload (image-resize.ts): Android Bitmap.compress, iOS UIImage.jpegData and the web canvas all write a new image without the camera's EXIF block, so its GPS tags never reach storage.

Questions only counsel can answer: (a) with location processed only on the device, is the 위치기반서비스사업자 신고 (item 3) still required, and does 제1조 ③ keep its filing clause? (b) does the location consent in the consent flow stay mandatory, or does the OS permission suffice when nothing leaves the device?

Risk if skipped: PIPC may treat AI-generated policy text as insufficient under §30's "easy-to-understand" requirement. More concretely: the cross-border transfer section (Supabase/Sentry/Expo Push) is the highest-enforcement-priority area in 2025 — counsel verifies the §28-8 disclosure language is precise enough to survive the Jan-2025 KakaoPay/Apple template.


⛔ 3. 위치기반서비스사업자 신고 at lbsc.kr — KCC mandatory before GPS feature goes live ​

Why: Location Information Act (위치정보의 보호 및 이용 등에 관한 법률) §9 requires a business that provides location-based services to file as a 위치기반서비스사업자 with the Korea Communications Commission (KCC) before launching them. GPS check-in (the original trigger) was retired by migration 00384; the remaining location uses (the pickup filters' default region, the venue finder's 가까운 순 sort, club-card distances) run on the device and send nothing to the server (inventory under item 2). Whether that still requires the filing is counsel's question (item 2, question a); until counsel answers, treat the filing as required.

There is a 1-month 소상공인 (small business) grace period after launch, but the safer posture is to file before public availability.

Action:

  1. Register a business entity (사업자등록) if not already done. Sole proprietor (개인사업자) suffices for the filing — corporate (법인) is not required.
  2. Go to https://lbsc.kr (KCC location-based-services registry).
  3. Choose 신고 (not 허가 — 허가 is for telco-level GPS providers).
  4. Required documents:
    • 사업자등록증
    • Service description (use the existing privacy-screen GPS section as the base; KCC accepts Korean-language plain-text descriptions)
    • Designated 위치정보관리책임자 — typically the same person as the CPO from item 1
    • Technical security measures description: foreground-only permission, one reading per use, processed on the device, never transmitted or stored; uploaded photos lose their EXIF location on the device (item 2's inventory)
  5. Filing fee: free for 신고.
  6. Process: KCC reviews within 1-3 weeks; approval letter arrives by email.
  7. Once approved, update PrivacyScreen "Location Information" section with the assigned 신고번호 + filing date.

Risk if skipped: Operating GPS features without the 신고 is a KRW 30M penalty under §43(3) + suspension order. Discovery vector is typically a user complaint → KCC audit. Korean media also covers these audits, so reputation cost can exceed the fine.


⛔ 4. iOS preview + production builds — gated on Apple Developer credentials ​

Why: Currently only Android builds exist on EAS (b3542ab9 production, e9483ebc preview). iOS App Store + TestFlight require:

  • An active Apple Developer Program enrollment (USD $99/year)
  • Either an Individual or Organization account (Organization is required for App Store Connect 'Team Members' separation, but Individual works for solo founders)
  • A configured App Store Connect app record (Bundle ID = com.ivorybridge.twomore, matching what's locked in app.config.js)

Action:

  1. Enroll in Apple Developer Program if not already (https://developer.apple.com/programs/enroll/). Allow 1-2 days for D-U-N-S verification on Organization accounts.
  2. Register the Bundle ID com.ivorybridge.twomore in Apple Developer → Certificates, Identifiers & Profiles → Identifiers → "+".
  3. Create the app record in App Store Connect (https://appstoreconnect.apple.com → My Apps → "+" → New App). Bundle ID dropdown should now show com.ivorybridge.twomore.
  4. Run from repo root:
    bash
    cd apps/mobile
    npx eas-cli credentials configure --platform ios
    # follow interactive prompts; EAS will mint a Distribution Certificate + Provisioning Profile
  5. Trigger first preview build:
    bash
    npx eas-cli build --profile preview --platform ios
  6. Once preview validates on a real device, trigger production build:
    bash
    npx eas-cli build --profile production --platform ios
  7. Upload to TestFlight from EAS Dashboard or via eas submit.

Risk if skipped: iOS launch is blocked. Android-only soft-launch is viable (Korea is ~30% iOS, ~70% Android, but the tennis demographic skews iOS-heavier — likely 45%+ of TAM unreachable without iOS).


⛔ 5. Branch protection on main ​

Why: CI is reliably green (every push runs typecheck + tests + lint:strict) but main is currently writable by anyone with push access. A bad merge with no CI gate could land directly. Branch protection makes CI a hard requirement, prevents force-push, and ensures the linear-history we rely on for the verify-no-stale-claims gate.

Action:

  1. GitHub Settings → Branches → https://github.com/ivorybridge-2025/twomore-v2/settings/branches
  2. Click "Add classic branch protection rule" (Classic, not "Rulesets" — simpler and adequate for solo repos).
  3. Branch name pattern: main
  4. Enable:
    • [x] Require status checks to pass before merging
      • Required check: typecheck + tests + lints (the existing CI job name from .github/workflows/ci.yml)
      • Do NOT require drift — architectural erosion (non-blocking by design — runs alongside but doesn't gate)
    • [x] Require linear history (preserves verify-no-stale-claims clean commits)
    • [x] Do not allow bypassing the above settings (apply to admins too)
  5. Save.

Risk if skipped: A red-CI merge could land on main and break the next OTA. Low frequency, but the cost of one bad OTA hitting production users is high enough to justify the 2-minute setup.


➖ 6. SUPABASE_ACCESS_TOKEN repo secret (type-drift CI gate) ​

Why: .github/workflows/ci.yml has an opt-in job that compares the committed generated.types.ts against the live Supabase schema. Without the token, the job skips (silent green). With it, the job catches "schema changed in dashboard but types weren't regenerated" drift.

Action:

  1. Supabase Dashboard → Settings → Access Tokens → "Generate new token".
  2. GitHub repo Settings → Secrets and variables → Actions → New repository secret.
  3. Name: SUPABASE_ACCESS_TOKEN. Paste token.
  4. Next CI run automatically picks it up.
  5. Bonus: also regenerate generated.types.ts locally once via:
    bash
    npx supabase gen types typescript --linked \
      | npx prettier --config .prettierrc.json --parser typescript \
      > packages/app/src/adapters/supabase/generated.types.ts
    Commit only if the diff is intentional.

Risk if skipped: Non-blocking. Schema drift catches will be late (next time someone manually runs the gen command), but no production impact.


⛔ 7. Production EAS Update channel + OTA pipeline — launch-gated ​

Why: Production users need JS-layer bugfixes without a full app-store resubmission.

Current state:

  1. apps/mobile/eas.json sets production.channel = "production".
  2. package.json provides guarded yarn ota:production.
  3. package.json also provides guarded yarn ota:production:canary for a 5% rollout.
  4. scripts/guard-ota-release.mjs blocks OTA from dirty, no-upstream, unpushed-ahead, or behind-upstream source states, and blocks production OTA unless TWOMORE_ALLOW_PRODUCTION_OTA=1 is set.

Current policy: before public launch, publish OTA updates only to preview:

bash
yarn ota:preview

Any significant app change must first be classified through Release Versioning And OTA Workflow: docs-only changes do not publish OTA, JS-only changes may publish preview OTA, and native/runtime changes require a preview build instead of OTA-only shipping. Artifact IDs, hosted smoke outcomes, failed attempts, and explicit no-artifact decisions are recorded in Release Evidence Ledger.

Remaining launch check: after R4 Launch Readiness is accepted, the first production OTA should be verified in EAS Dashboard → Updates and smoke-tested on an installed production build. Use canary first when possible:

bash
TWOMORE_ALLOW_PRODUCTION_OTA=1 yarn ota:production:canary

➖ 8. App Store / Play Store listing assets ​

Why: Each store requires icons, screenshots, descriptions, privacy questionnaires before approval.

Action checklist:

Apple App Store Connect:

  • App icon (1024x1024 PNG) — use apps/mobile/assets/icon.png (already generated, 1024x1024)
  • iPhone 6.7" screenshots (1290x2796) — capture 3-5 from live preview build
  • iPad screenshots (if supporting iPad)
  • App description (Korean — 4000 char limit), keywords, support URL, marketing URL
  • App Privacy questionnaire — data types collected: identifiers (Supabase auth), contact info (email, phone), location (GPS), purchases (Portone — future), usage data (analytics), diagnostics (Sentry). Cross-border = yes (Supabase US, Sentry US, Expo Push US).
  • Age rating: 12+ (per the under-14 PIPA gate; lower than 17+ since no objectionable content)

Google Play Console:

  • App icon (512x512 PNG) — generate from apps/mobile/assets/adaptive-icon.png
  • Feature graphic (1024x500)
  • Phone screenshots (1080x1920 minimum)
  • App description (Korean), short description
  • Data safety section — same data types as App Privacy
  • Content rating questionnaire

Risk if skipped: Store submission rejection or month-long review delays.


✅ 9. App-update gating (force-update path) ​

Built 2026-09-24 (migration 00669). No credential, vendor console, or owner action was needed — this was repo work throughout. Only the "update now" button's destination still depends on an external input (the live store listing URLs, EXT-8) — the button simply doesn't render until those exist.

Why: When you ship a future native-module change that breaks API compatibility (e.g. a new runtime version, a Supabase client major bump), users on old builds can crash repeatedly until they upgrade. The app previously had no force-update mechanism.

What shipped: A single hard gate, not a two-tier banner/gate split — simpler, and the min-version keys are seeded to the CURRENT binary so nothing is blocked today. public.get_app_version_policy(p_platform) (SECURITY DEFINER, anon+authenticated EXECUTE, runs before login) reads app_min_runtime_<platform> / app_store_url_<platform> off public.app_config. The client compares its own Updates.runtimeVersion (expo-updates) against that minimum (isRuntimeSupported, packages/app/src/domain/rules/app-version-gate.rules.ts) via useAppVersionGate, and AppVersionGate (mounted around <AuthGate /> in apps/mobile/app/_layout.tsx) swaps in UpdateRequiredScreen instead of the app when it fails. Fails open by construction: web, a dev build, offline, a query error, and a missing/blank config value all resolve to 'ok' — see docs/canon/data-and-hooks.md. To raise the minimum: update the app_config row directly, no deploy needed. Wire real store URLs once EXT-8 lands (app_store_url_android/app_store_url_ios, currently '').


✅ 10. Sentry sample-rate dial-back — tracesSampleRate / profilesSampleRate ​

Why: Full Sentry performance/profiling capture is useful for investigations but too expensive for public traffic.

Current state: packages/app/src/lib/sentry.ts uses launch defaults:

ts
tracesSampleRate: 0.2;
profilesSampleRate: 0.1;

Raise temporarily only when diagnosing a specific regression.


➖ 11. Dev-surface production gating — DevPanel + seed/simulate edge functions ​

Why: The in-app DevPanel (FAB + scenario seeder + telemetry/diagnostics) and the destructive seed edge functions must never be reachable by real users. They ARE gated today — this item is a verify-before-launch checkpoint, not new work.

Current gating (verify each still holds):

  1. DevPanel visibility — packages/app/src/presentation/components/dev-panel/index.tsx returns null unless devMode || isDevContext() || userId === DEV_USER_ID. isDevContext() is __DEV__ || NODE_ENV !== 'production' — both false in a production build. So the FAB only shows for the dev account or a dev/preview build. Confirm a release build with a non-dev account shows no FAB.
  2. Telemetry RPCs — dev_read_telemetry / dev_clear_telemetry (migration 00198) raise 42501 for any caller whose auth.uid() ≠ DEV_USER_ID, so even if the panel rendered, a non-dev user reads/clears nothing. Server-side enforced.
  3. Destructive seed edge functions — seed-scenario / simulate-activity reject non-local / non-allowlisted invocations (production-env guards + the SEED_ALLOWED_UIDS allowlist, defaulting to the dev account). Confirm seed-scenario is NOT callable by an arbitrary authenticated user in production.

Action: On the first production build, sign in with a NON-dev account and confirm: (a) no DevPanel FAB on the home tab, (b) supabase.rpc('dev_read_telemetry') returns a 42501 error, (c) seed-scenario invocation is rejected. Document the check in the rebuild-log.

Before that check (added 2026-09-08): run npx supabase secrets unset SEED_PROD_UNLOCK and redeploy seed-scenario. The pre-launch owner unlock (two secrets: SEED_PROD_UNLOCK=true + SEED_ALLOWED_UIDS) lets the dev panel seed the hosted project from the phone while it is the only QA bed; with the flag gone the function is fail-closed again regardless of the allowlist.

Seed storage (added 2026-09-25): empty and delete the dev-only seed-fixtures bucket through the Storage API (dashboard, or npx supabase storage rm -r ss:///seed-fixtures --experimental --linked --yes, then delete the bucket). It holds the seed world's photos and logos, is private, and is declared in supabase/config.toml, so drop that declaration too. Clubs seeded before launch carry copies of the fixtures in their own paths; those go with the seeded clubs. Keep club-default-photos. It is a product bucket again since 2026-09-26: it serves the 27 default club covers (COVER-ALWAYS, canon/clubs.md) at covers/<key>.jpg. On the production project, run node scripts/club-default-covers.mjs --linked once, then remove the old <key>/card.jpg/<key>/detail.jpg objects from the retired set. Migration 00673's removal of client writes stays right: only that script writes there.

Risk if skipped: If any gate regressed, a real user could wipe/seed production data or read the dev account's telemetry. All three gates exist today; this is a regression-catch checkpoint, hence ➖ (verify, don't build).


Why: R1 sharing now uses canonical https://twomore.app URLs, and the app has custom-scheme recipient proof. Public launch still needs verified HTTP iOS Universal Links and Android App Links so recipients can tap a normal web URL from KakaoTalk, browser, or SMS and land in the installed app without relying on twomore://.

Current state:

  1. apps/mobile/app.json declares ios.associatedDomains = ["applinks:twomore.app"].
  2. apps/mobile/app.json declares Android autoVerify intent filters for /s, /m, /p, /lb, and /join-club.
  3. apps/web/app/.well-known/apple-app-site-association/route.ts serves the iOS association response when TWOMORE_APPLE_TEAM_ID is set.
  4. apps/web/app/.well-known/assetlinks.json/route.ts serves the Android association response when TWOMORE_ANDROID_SHA256_CERT_FINGERPRINTS is set.
  5. yarn check:r1-app-links verifies the static repo contract and reports the missing external identifiers.
  6. yarn check:r1-app-links:remote is the live-domain proof gate. As of 2026-06-28 it fails before credential comparison because both live association endpoints redirect from the canonical apex host to www: https://twomore.app/.well-known/apple-app-site-association redirects to https://www.twomore.app/.well-known/apple-app-site-association, and https://twomore.app/.well-known/assetlinks.json redirects to https://www.twomore.app/.well-known/assetlinks.json. The www target then redirects the .well-known requests to /auth/login instead of serving association JSON.
  7. Android preview build 6aa365f6-7814-4250-a708-a27843a919dc was inspected with apksigner verify --print-certs on 2026-06-28. Its public signing certificate SHA-256 fingerprint is F4:67:1F:16:B3:95:12:06:0C:20:4C:E4:5E:AA:79:F8:6A:C1:AD:C9:29:49:F4:D8:AE:EB:DB:6A:D3:D2:8B:3E. Treat this as the observed preview signing fingerprint, not production proof; recheck the final candidate build if signing credentials rotate.

Action:

  1. Obtain Apple Team ID from Apple Developer membership.
  2. Obtain or confirm SHA-256 certificate fingerprints for the EAS/Play signing certificates that should verify com.ivorybridge.twomore. The current observed preview Android fingerprint is listed above; confirm it still matches the candidate build before final acceptance.
  3. Set web deploy environment variables: TWOMORE_APPLE_TEAM_ID and TWOMORE_ANDROID_SHA256_CERT_FINGERPRINTS.
  4. Deploy the web app behind the canonical apex host https://twomore.app without redirecting the two .well-known association endpoints to www.
  5. Run yarn check:r1-app-links.
  6. Run yarn check:r1-app-links:remote and require direct HTTP 200 JSON for both association endpoints.
  7. Trigger a preview build because apps/mobile/app.json native config changed; do not treat OTA as sufficient.
  8. Install the preview build on iOS and Android and verify these URLs open the installed app:
    • https://twomore.app/s/<sessionId>
    • https://twomore.app/m/<matchId>
    • https://twomore.app/p/<userId>
    • https://twomore.app/lb/<clubId>
    • https://twomore.app/join-club?code=<inviteCode>
  9. Record the build ID, device smoke, endpoint checks, and any rollback pointer in Release Evidence Ledger.

Risk if skipped: HTTP share links remain web-preview/custom-scheme only and R1-3 cannot be accepted as a full web-to-installed-app sharing loop.


Why: Korean club acquisition is expected to happen through KakaoTalk. R1 share cards now have a source-level Kakao template contract and fallback-safe native adapter wiring, but the app still needs credentialed native-build proof before Kakao-native sharing can be accepted. Public launch should not claim Kakao-native sharing until approved Kakao templates render in KakaoTalk and return recipients to useful app/web destinations.

Current state:

  1. packages/app/src/config/kakao-share-templates.json defines the R1 custom-template manifest for session invite, club invite, match result, tier promotion, stat card, and leaderboard shares.
  2. packages/app/src/presentation/utils/share-cards.ts attaches kakaoTemplate metadata to every R1 share-card composer.
  3. scripts/check-r1-kakao-share.mjs verifies static manifest coverage and reports missing external gates.
  4. yarn check:r1-kakao-share passes static coverage.
  5. yarn check:r1-kakao-share:credentials intentionally fails until all six TWOMORE_KAKAO_TEMPLATE_* IDs and either TWOMORE_KAKAO_NATIVE_APP_KEY or the existing EXPO_PUBLIC_KAKAO_APP_KEY fallback are available.
  6. @react-native-kakao/core and @react-native-kakao/share are installed in the mobile app shell and @twomore/app.
  7. apps/mobile/app.config.js conditionally configures the Kakao core native plugin when TWOMORE_KAKAO_NATIVE_APP_KEY exists, falling back to EXPO_PUBLIC_KAKAO_APP_KEY for environments that already carry the Kakao Native App Key under the older login variable.
  8. packages/app/src/adapters/share/share.native.ts tries Kakao custom-template sharing and falls back to the OS share sheet.
  9. No preview build/device proof exists yet for KakaoTalk template rendering.

Action:

  1. In Kakao Developers, create and approve the six custom templates represented by the manifest keys:
    • session_invite
    • club_invite
    • match_result
    • tier_promotion
    • stat_card
    • leaderboard
  2. Set the template IDs in the appropriate preview environment: TWOMORE_KAKAO_TEMPLATE_SESSION_INVITE, TWOMORE_KAKAO_TEMPLATE_CLUB_INVITE, TWOMORE_KAKAO_TEMPLATE_MATCH_RESULT, TWOMORE_KAKAO_TEMPLATE_TIER_PROMOTION, TWOMORE_KAKAO_TEMPLATE_STAT_CARD, and TWOMORE_KAKAO_TEMPLATE_LEADERBOARD.
  3. Set TWOMORE_KAKAO_NATIVE_APP_KEY in the preview build environment. If the target environment already exports EXPO_PUBLIC_KAKAO_APP_KEY with the same Kakao Native App Key, the native share plugin will use that value, but the TWOMORE_ name remains preferred for R1 release evidence.
  4. Create a preview build that includes the Kakao native plugin and share dependencies; do not ship this as OTA-only.
  5. Run:
    bash
    yarn check:r1-kakao-share
    yarn check:r1-kakao-share:credentials
    yarn workspace @twomore/app test src/adapters/share/__tests__/share.native.test.ts --runInBand
    yarn workspace @twomore/mobile preflight
    yarn check
  6. Smoke on installed iOS and Android preview builds with KakaoTalk installed: verify each R1 template renders expected title/body/buttons and returns to a useful /s, /m, /p, /lb, or /join-club?code=<inviteCode> destination.
  7. Record template IDs' environment target, preview build ID, device smoke results, fallback behavior, and rollback pointer in Release Evidence Ledger.

Risk if skipped: R1 sharing remains generic OS/web sharing. That is usable, but it does not prove the Kakao-native growth loop the roadmap depends on.


⛔ 14. Owned visual assets — Higgsfield, before production ​

Why: the app ships on code-drawn geometry (the onboarding intro pager that carried it retired in W4 batch 24; the share cards and store assets remain). Owner decision 2026-09-18: generate one owned, consistent set before production, using the Higgsfield model through its MCP connector.

Club default covers are not part of this scope (2026-09-26): a club without its own photo shows one of 27 licensed court scenes (CLUB_DEFAULT_COVERS, packages/app/src/config/club-default-covers.ts; COVER-ALWAYS in canon/clubs.md). They are real photographs under licences that need no credit, so they stay out of the generated set. The rest of the asset set below still stands.

Scope (produced once, committed — never generated at runtime):

  1. Onboarding value-prop illustrations — retired with the pager (W4 batch 24, owner-agreed 2026-11-11); a first-start illustration is a later owner call.
  2. Share-card backgrounds for the R1 Kakao/OS share cards.
  3. Store listing assets (feeds #8) and a store preview video.

Sequence:

  1. Owner adds the Higgsfield connector (claude.ai connector settings) and confirms the commercial-use terms on generated outputs in writing here.
  2. Claude drafts one style prompt for the whole set and renders candidates onto a wireframe board (/wireframes/asset-set) — one phone per surface, no faces, checked against the flat/graphics-led canon.
  3. Owner picks; Claude commits the illustration files under apps/mobile/assets/, and records the licence evidence in Release Evidence Ledger.

Risk if skipped: onboarding art that does not match the rest of the product's visual language.


Quick scan ​

Open these files when you have the values:

ItemFileLines
CPO contactpackages/app/src/config/legal/documents/privacy.ko.ts221–225 (§11)
Policy version bumppackages/app/src/config/legal/policy-versions.ts20–43
GPS regulatory mentionpackages/app/src/config/legal/documents/privacy.ko.ts— search 위치정보
EAS iOS configapps/mobile/app.config.js— ios section
CI workflow.github/workflows/ci.yml—
Sentry sample ratepackages/app/src/lib/sentry.ts— tracesSampleRate / profilesSampleRate
DevPanel prod gatingpackages/app/src/presentation/components/dev-panel/index.tsx— enabled guard
HTTP app linksapps/mobile/app.json— associatedDomains / intentFilters
Kakao share templatespackages/app/src/config/kakao-share-templates.json— template keys / env vars

Out of scope for this checklist ​

These are tracked elsewhere and don't gate public launch:

  • Production Kakao auth and real PortOne native adapter/console secrets → external-credential/runtime work tracked in CLAUDE.md
  • Coaching marketplace → Phase 9
  • Court booking SaaS → Phase 8
  • Inter-club + seasons + chat → Phase 11
  • Migration of legacy ANON_KEY → PUBLISHABLE_KEY in client.ts → Path B continuation (not blocking; both keys are valid)
  • Web app launch (apps/web) → separate gate

Markdown remains the source of truth. Run yarn docs:check before handoff.