diff --git a/docs/app-store/README.md b/docs/app-store/README.md index 0c9a76f..b83ee97 100644 --- a/docs/app-store/README.md +++ b/docs/app-store/README.md @@ -5,6 +5,11 @@ Store**. The engineering is done on both platforms; what remains is account setup, listing assets, and the submit flow. These docs cover the parts that live in the repo; the account-gated steps are called out as "you". +> **1.0 launch (2026-06):** ship **1.0.0** to both stores **in parallel** — see +> [ADR-004](../decisions/ADR-004-app-store-1.0.md) (supersedes ADR-003's MAS +> deferral). Versions are bumped in-repo. **Start here:** +> [launch-1.0-checklist.md](./launch-1.0-checklist.md). + ## Status at a glance | | iOS App Store | Mac App Store | @@ -16,6 +21,7 @@ in the repo; the account-gated steps are called out as "you". ## Documents +- **[launch-1.0-checklist.md](./launch-1.0-checklist.md)** — ⭐ the actionable 1.0 launch checklist for both stores (start here). - **[ios-submission.md](./ios-submission.md)** — iOS App Store runbook (TestFlight → App Store). - **[MAS-publishing-plan.md](./MAS-publishing-plan.md)** — Mac App Store plan (sandbox, certs, build flavor, review gotchas, phased plan). - **[signing-setup.md](./signing-setup.md)** — Developer ID signing for the **direct-download** macOS DMG (separate from MAS). diff --git a/docs/app-store/launch-1.0-checklist.md b/docs/app-store/launch-1.0-checklist.md new file mode 100644 index 0000000..a836baf --- /dev/null +++ b/docs/app-store/launch-1.0-checklist.md @@ -0,0 +1,75 @@ +# 1.0 App Store launch — checklist + +Target: **1.0.0** on the **iOS App Store** and the **Mac App Store**, in parallel +(see [ADR-004](../decisions/ADR-004-app-store-1.0.md)). The direct-download DMG +channel stays live alongside MAS. + +Versions are already bumped in-repo: +- iOS `MARKETING_VERSION = 1.0.0` → build **100000** (derived; > prior 2100) +- desktop `1.0.0` in `package.json` / `src-tauri/tauri.conf.json` / `Cargo.toml` (+`Cargo.lock`) + +Repo-side prep is done. Everything below is **account-gated** unless marked 🤖. + +Legend: 🔑 needs your Apple account / portal · 🤖 I can run/drive · ⏳ wait/review · ✅ done + +--- + +## A. Shared prerequisites +- [x] ✅ Apple Developer Program active — Team `9LH9NBX7P4` (Bihao Wang), `wangharp@gmail.com` +- [x] ✅ Privacy Policy URL live — https://github.com/oratis/Markup/blob/main/PRIVACY.md +- [x] ✅ Listing copy (EN+中文), privacy-label answers, App Review notes, reviewer sample vault — in `docs/app-store/` +- [ ] 🔑 Screenshots (see §D) + +--- + +## B. iOS App Store (`com.appkon.markup.ios`, ASC app `6775530509`) +Binary pipeline already proven (EAS → App Store Connect). Remaining: + +1. 🤖 Build 1.0.0 to ASC: `cd ios && ./scripts/build-ios.sh production` (build 100000). *I can trigger this on your say-so.* +2. ⏳ ~5–10 min ASC processing. +3. 🔑 App Store Connect → Markup → **+ Version → 1.0.0**: + - Attach build **100000** + - Paste metadata from [`listing-copy.md`](./listing-copy.md) — name, subtitle, keywords, description, promo text, support/marketing/privacy URLs, **category Productivity**, **age 4+**, copyright + - **App Privacy → "Data Not Collected"** (answers in [`privacy-label-and-review.md`](./privacy-label-and-review.md)) + - Upload screenshots — iPhone 6.9″ (1320×2868) + 6.5″ (1242×2688) + iPad 13″ (2064×2752) + - **App Review notes** + sample-vault pointer ([`privacy-label-and-review.md`](./privacy-label-and-review.md)) +4. 🔑 **Submit for Review.** + +--- + +## C. Mac App Store (`com.appkon.markup`) — first-time MAS setup +Code is ready; this is new account/cert work. + +1. 🔑 developer.apple.com → **Identifiers** → register App ID `com.appkon.markup` with the **App Sandbox** capability. +2. 🔑 Create two certificates (CSR from Keychain Access → Certificate Assistant): + - **Apple Distribution** — signs the `.app` + - **Mac Installer Distribution** ("3rd Party Mac Developer Installer") — signs the `.pkg` +3. 🔑 Create a **Mac App Store** provisioning profile for `com.appkon.markup` + the Apple Distribution cert → download `Markup_MAS.provisionprofile`. +4. 🔑 Add GitHub repo **Secrets → Actions** (Settings → Secrets and variables → Actions): + - `MAS_CERT_P12_BASE64` — base64 of a `.p12` holding **both** certs + private keys + - `MAS_CERT_P12_PASSWORD` — the `.p12` export passphrase + - `MAS_APP_IDENTITY` — e.g. `Apple Distribution: Bihao Wang (9LH9NBX7P4)` (exact string from Keychain) + - `MAS_INSTALLER_IDENTITY` — e.g. `3rd Party Mac Developer Installer: Bihao Wang (9LH9NBX7P4)` + - `MAS_PROVISION_PROFILE_BASE64` — base64 of the `.provisionprofile` +5. 🤖 Produce the `.pkg`: push tag **`v1.0.0`** → CI `mas` job runs `scripts/build-mas.sh` → uploads `Markup.pkg` artifact (7-day retention). *I can create + push the tag once the secrets exist.* (Local alt: run `scripts/build-mas.sh` with the three `MAS_*` env vars.) +6. 🔑 App Store Connect → **+ New App** (macOS) for `com.appkon.markup`: SKU, **Free**, metadata from [`listing-copy.md`](./listing-copy.md), **"Data Not Collected"**, screenshots (1280×800 or 2560×1600, ≥1). +7. 🔑 Upload `Markup.pkg` — **Transporter.app** (drag in) or + `xcrun altool --upload-app -f Markup.pkg -t macos --apple-id wangharp@gmail.com --password `. +8. 🔑 Attach build → **Submit for Review.** + +Background + gotchas: [`MAS-publishing-plan.md`](./MAS-publishing-plan.md). + +--- + +## D. Screenshots +- **iOS** 🔑 — needs a Simulator/device. (I can't run an iOS Simulator here — Command Line Tools only; the app target builds in CI/Xcode.) You capture, or we automate later with a dedicated screenshot scheme. +- **macOS** 🤖 — I can build + launch the app and capture, using `docs/app-store/reviewer-sample-vault/` for content, if you want. + +--- + +## What I can drive next (just say which) +- **B1** — trigger the iOS 1.0.0 build to App Store Connect now. +- **C5** — once the `MAS_*` secrets are in, push `v1.0.0` to produce `Markup.pkg`. +- **D (macOS)** — generate Mac screenshots from the sample vault. + +Release notes for the tag: [`../release-notes-v1.0.0.md`](../release-notes-v1.0.0.md). diff --git a/docs/decisions/ADR-004-app-store-1.0.md b/docs/decisions/ADR-004-app-store-1.0.md new file mode 100644 index 0000000..c8d5037 --- /dev/null +++ b/docs/decisions/ADR-004-app-store-1.0.md @@ -0,0 +1,33 @@ +# ADR-004: 1.0 双端上架 App Store(iOS + Mac App Store) + +- **状态**: 已批准(2026-06-14) +- **日期**: 2026-06-14 +- **关系**: 取代 [ADR-003](./ADR-003-positioning-and-distribution.md) 第 3 条("1.0 走直分发优先,MAS 推迟为 1.0 后增量")的**分发时序**部分。ADR-003 的产品定位(阅读器优先 × 知识库、GitHub 主打叙事)继续有效。 + +## 上下文 + +ADR-003(2026-06-10)当时决定 1.0 仅以直分发 DMG 发布,把 Mac App Store 推迟到 1.0 之后,理由是不让账号/审核流程阻塞 1.0。 + +此后情况变化: + +- iOS 已在 TestFlight 稳定迭代(最新 0.2.10 / build 2100),App Store 提交侧仅剩账号操作(截图、列表、提交审核),二进制流水线(EAS → App Store Connect)已验证。 +- 桌面端 MAS 脚手架已全部就绪并经沙箱验证:`src-tauri/tauri.mas.conf.json`、`Entitlements.mas.plist`、`scripts/build-mas.sh`、CI 的 `mas` job、持久化安全作用域书签(`tauri-plugin-persisted-scope`),更新器在 MAS 构建中编译关闭。 +- 直分发 DMG(已签名公证 + 自动更新)已稳定运行多个版本,可与 MAS 并存。 + +用户决定:1.0 同时上架 **iOS App Store 与 Mac App Store**,不再推迟 MAS。 + +## 决策 + +1. **1.0 双端同时上架 App Store**:iOS 与 macOS 均以 1.0.0 提交各自的 App Store。直分发 DMG 渠道**继续保留**——MAS 与直分发并行,互不取代。 +2. **版本统一为 1.0.0**:iOS(`MARKETING_VERSION` 1.0.0 / build 100000)与桌面(`package.json` / `tauri.conf.json` / `Cargo.toml` 均 1.0.0)在 1.0 公开发布里程碑对齐;此后两端各自按 SemVer 独立演进。 +3. 据此,**ADR-003 第 3 条"MAS 推迟"作废**;其余条款(定位、GitHub 主打)不变。 + +## 后果 + +- **正面**:双端同时获得 App Store 触达;MAS 代码债为零(早已就绪)。 +- **代价**:1.0 发布新增账号侧关键路径——MAS 需要 App ID(启用 App Sandbox)、Apple Distribution + Mac Installer Distribution 两张证书、Mac App Store Provisioning Profile,以及 GitHub 的 `MAS_*` secrets;两端都需各自的 App Store Connect 列表与截图。逐项见 [docs/app-store/launch-1.0-checklist.md](../app-store/launch-1.0-checklist.md)。 +- **可逆性**:分发是叠加式选择,保留直分发 DMG;若 MAS 审核受阻,不影响 iOS 与直分发渠道。 + +## 用户确认结果(2026-06-14) + +用户指示"开始准备 iOS 和 Mac 版都上架 App Store",并在两项上明确确认:版本号(两端 1.0.0)、MAS 时序(与 iOS 并行,覆盖 ADR-003)。 diff --git a/docs/release-notes-v1.0.0.md b/docs/release-notes-v1.0.0.md new file mode 100644 index 0000000..d5f73fa --- /dev/null +++ b/docs/release-notes-v1.0.0.md @@ -0,0 +1,31 @@ +# Markup v1.0.0 — 1.0, now on the App Store + +Markup hits **1.0** and arrives on the **iOS App Store** and the **Mac App +Store**, alongside the existing signed + notarized direct-download DMG. iOS and +desktop are unified at 1.0.0 for the public launch. + +## 📱 iOS — on the App Store +The reader-first Markdown app for iPhone & iPad: open a folder from Files/iCloud +(your Mac's vault) **or open a GitHub repo as a vault** and read it offline — +rendered code, math, diagrams, tables, callouts, wikilinks, search, and tabs. + +## 🖥 macOS — on the Mac App Store +The Typora-spirit WYSIWYG editor, now sandboxed for the Mac App Store (your +chosen vault folder persists across launches via security-scoped bookmarks). +Reader-first rendering, **open a GitHub repo as a vault**, site-style navigation +(prev/next pager, Section rail, in-page `#heading` links), and editor callouts. + +## 🔒 Private by default +No account, no telemetry, no tracking — **Data Not Collected**. Your notes stay +on your device and your own iCloud/GitHub. Network access only for user-initiated +GitHub fetches. + +## 📦 Files (direct download) +- `Markup_1.0.0_apple-silicon.dmg` / `Markup_1.0.0_intel.dmg` +- `latest.json` + `.app.tar.gz` (auto-updater) +- `SHA256SUMS` + +Signed + notarized; updates automatically from v0.6.1+. (The Mac App Store build +updates through the App Store instead.) + +🤖 Generated with [Claude Code](https://claude.com/claude-code) diff --git a/ios/MarkupApp/MarkupApp.xcodeproj/project.pbxproj b/ios/MarkupApp/MarkupApp.xcodeproj/project.pbxproj index 9a3af2b..18bc446 100644 --- a/ios/MarkupApp/MarkupApp.xcodeproj/project.pbxproj +++ b/ios/MarkupApp/MarkupApp.xcodeproj/project.pbxproj @@ -215,7 +215,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.2.10; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.appkon.markup.ios; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -245,7 +245,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.2.10; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.appkon.markup.ios; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/ios/MarkupApp/MarkupApp/ReaderView.swift b/ios/MarkupApp/MarkupApp/ReaderView.swift index 4c8e7a6..021412c 100644 --- a/ios/MarkupApp/MarkupApp/ReaderView.swift +++ b/ios/MarkupApp/MarkupApp/ReaderView.swift @@ -174,6 +174,7 @@ struct ReaderView: View { // is (re)written on every render-input change via `.task(id:)`. // We never write into user-picked folders. ReaderWebView( + html: html, fileURL: renderedSiblingURL, readAccessURL: vault.rootURL, loadToken: htmlReloadToken, diff --git a/ios/MarkupApp/MarkupApp/ReaderWebView.swift b/ios/MarkupApp/MarkupApp/ReaderWebView.swift index 05ed5ef..dc5b2b1 100644 --- a/ios/MarkupApp/MarkupApp/ReaderWebView.swift +++ b/ios/MarkupApp/MarkupApp/ReaderWebView.swift @@ -143,15 +143,31 @@ struct ReaderWebView: UIViewRepresentable { context.coordinator.onInRepoDoc = onInRepoDoc context.coordinator.onFinishLoad = onFinishLoad context.coordinator.loadedFilePath = fileURL?.standardizedFileURL.path + // In-memory HTML to fall back to if the file:// load fails or the file + // isn't on disk yet — so the reader never shows a blank/black page. + context.coordinator.fallbackHTML = html + context.coordinator.fallbackBaseURL = baseURL ?? fileURL?.deletingLastPathComponent() let key = (fileURL.map { "file:" + $0.path } ?? html) + "#\(loadToken)" if context.coordinator.lastHTML != key { context.coordinator.lastHTML = key context.coordinator.loaded = false - if let fileURL { + context.coordinator.didFallback = false + if let fileURL, FileManager.default.fileExists(atPath: fileURL.path) { + // On-disk doc (raw .html, or an app-owned vault's rendered + // sibling): a file load grants read access so relative + // images/CSS resolve against the working copy. + webView.loadFileURL( + fileURL, allowingReadAccessTo: readAccessURL ?? fileURL.deletingLastPathComponent()) + } else if !html.isEmpty { + // No file on disk yet (an app-owned sibling is written just + // after first paint) — render the in-memory HTML now so the page + // is never blank; the post-write token bump reloads from the + // file for full fidelity (relative images). + webView.loadHTMLString(html, baseURL: baseURL ?? fileURL?.deletingLastPathComponent()) + } else if let fileURL { + // Raw .html doc with no fallback string: load it directly. webView.loadFileURL( fileURL, allowingReadAccessTo: readAccessURL ?? fileURL.deletingLastPathComponent()) - } else { - webView.loadHTMLString(html, baseURL: baseURL) } } @@ -182,6 +198,12 @@ struct ReaderWebView: UIViewRepresentable { /// document has finished loading (so a push has somewhere to land). var lastLive: String? var loaded = false + /// In-memory HTML + base to fall back to when a file:// load fails, so a + /// read-access/not-found error degrades to a readable page, not black. + /// `didFallback` guards against re-entering the fallback for one load. + var fallbackHTML = "" + var fallbackBaseURL: URL? + var didFallback = false init(onScroll: @escaping (Double) -> Void, onToggleTask: @escaping (Int) -> Void) { self.onScroll = onScroll @@ -213,6 +235,21 @@ struct ReaderWebView: UIViewRepresentable { onFinishLoad() } + // A document load failed (e.g. a file:// read-access denial, or the + // app-owned sibling wasn't on disk). Degrade to the in-memory HTML so the + // reader shows the document instead of a blank/black page. Deliberate + // cancels (in-repo link interception, external-link opens) are ignored. + func webView( + _ webView: WKWebView, didFailProvisionalNavigation _: WKNavigation!, withError error: Error + ) { + let ns = error as NSError + let cancelled = (ns.domain == NSURLErrorDomain && ns.code == NSURLErrorCancelled) + || (ns.domain == "WebKitErrorDomain" && ns.code == 102) // interrupted by policy + guard !cancelled, !fallbackHTML.isEmpty, !didFallback else { return } + didFallback = true + webView.loadHTMLString(fallbackHTML, baseURL: fallbackBaseURL) + } + func userContentController( _ controller: WKUserContentController, didReceive message: WKScriptMessage ) { diff --git a/package.json b/package.json index a99326e..c0ac480 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "markup", "private": true, - "version": "0.8.0", + "version": "1.0.0", "type": "module", "description": "High-performance Markdown editor for macOS — Typora-spirit clone", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4d23782..43c8e3d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2578,7 +2578,7 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup" -version = "0.8.0" +version = "1.0.0" dependencies = [ "ammonia", "base64 0.22.1", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f5cf5db..65310d3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "markup" -version = "0.8.0" +version = "1.0.0" description = "High-performance Markdown editor for macOS" authors = ["appkon"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index cb7d5e6..1e1c055 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Markup", - "version": "0.8.0", + "version": "1.0.0", "identifier": "com.appkon.markup", "build": { "frontendDist": "../dist",