diff --git a/.asc/screenshots.json b/.asc/screenshots.json new file mode 100644 index 00000000..938fe934 --- /dev/null +++ b/.asc/screenshots.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "app": { + "bundle_id": "ai.javachat", + "udid": "booted", + "output_dir": "./screenshots/raw/en-US/iphone" + }, + "steps": [ + { + "action": "launch" + }, + { + "action": "wait", + "duration_ms": 8000 + }, + { + "action": "screenshot", + "name": "01-welcome" + } + ] +} diff --git a/.asc/shots.settings.json b/.asc/shots.settings.json new file mode 100644 index 00000000..4fd453c6 --- /dev/null +++ b/.asc/shots.settings.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "app": { + "bundle_id": "ai.javachat", + "project": "mobile/iosApp/JavaChat.xcodeproj", + "scheme": "JavaChat", + "simulator_udid": "booted" + }, + "paths": { + "plan": ".asc/screenshots.json", + "raw_dir": "./screenshots/raw/en-US/iphone", + "framed_dir": "./screenshots/final/en-US/iphone" + }, + "pipeline": { + "frame_enabled": true, + "upload_enabled": false + }, + "upload": { + "version_localization_id": "d4c9161f-0343-47ed-b4f0-73f1cf37a424", + "device_type": "IPHONE_69", + "source_dir": "./screenshots/final/en-US/iphone" + } +} diff --git a/.asc/workflow.json b/.asc/workflow.json new file mode 100644 index 00000000..7a9e6568 --- /dev/null +++ b/.asc/workflow.json @@ -0,0 +1,57 @@ +{ + "env": { + "APP_ID": "6796580187", + "VERSION": "1.0", + "METADATA_PATH": "metadata", + "SCREENSHOT_PATH": "screenshots/final/en-US" + }, + "before_all": "asc auth status", + "workflows": { + "release-assets": { + "private": true, + "description": "Validates canonical metadata and every local screenshot set.", + "steps": [ + { + "name": "validate_metadata", + "run": "asc metadata validate --dir $METADATA_PATH" + }, + { + "name": "validate_iphone_screenshots", + "run": "asc screenshots validate --path $SCREENSHOT_PATH/iphone --device-type IPHONE_69" + }, + { + "name": "validate_ipad_screenshots", + "run": "asc screenshots validate --path $SCREENSHOT_PATH/ipad --device-type IPAD_PRO_3GEN_129" + }, + { + "name": "validate_mac_screenshots", + "run": "asc screenshots validate --path $SCREENSHOT_PATH/mac --device-type DESKTOP" + } + ] + }, + "ios-release-preflight": { + "description": "Validates local App Store assets and performs the read-only iOS release check.", + "steps": [ + { + "workflow": "release-assets" + }, + { + "name": "validate_ios_listing", + "run": "asc validate --app $APP_ID --version $VERSION --platform IOS" + } + ] + }, + "mac-release-preflight": { + "description": "Validates local App Store assets and performs the read-only Mac release check.", + "steps": [ + { + "workflow": "release-assets" + }, + { + "name": "validate_mac_listing_only", + "run": "asc validate --app $APP_ID --version $VERSION --platform MAC_OS" + } + ] + } + } +} diff --git a/.gitignore b/.gitignore index 4db76207..f0aa0418 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,19 @@ classpath.txt category/ findbugsfilter.xsd messages.xml + +### App Store Connect CLI (asc) transient state ### +### Repo-local config (.asc/*.json) stays tracked; run artifacts do not ### +.asc/runs/ +.asc/reports/ +.asc/release/checkpoints/ + +### App Store screenshot working artifacts ### +### Canonical assets stay tracked: screenshots/final/, screenshots/captions/ ### +screenshots/raw/ +screenshots/framed-test/ +screenshots/rejected/ +screenshots/review/ +screenshots/asc-review/ +### Stray debug/dogfood captures at the screenshots root (annotated browser shots) ### +/screenshots/*.png diff --git a/frontend/src/lib/composables/clerkAuthentication.svelte.test.ts b/frontend/src/lib/composables/clerkAuthentication.svelte.test.ts new file mode 100644 index 00000000..6c25f3f4 --- /dev/null +++ b/frontend/src/lib/composables/clerkAuthentication.svelte.test.ts @@ -0,0 +1,90 @@ +/** + * Covers the environment gates of {@link loadClerkAuthentication}: browsers + * that deny site storage (kiosk / hardened-privacy frames make the + * `window.localStorage` getter throw) and builds without a publishable key + * must disable auth quietly — no toast, no rejected promise, controls hidden. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { get } from "svelte/store"; + +const originalLocalStorageDescriptor = Object.getOwnPropertyDescriptor(window, "localStorage"); + +/** + * Mirrors Chromium's behavior when site data is blocked: the property getter + * itself throws a SecurityError before any storage method can be called. + */ +function denySiteStorageAccess(): void { + Object.defineProperty(window, "localStorage", { + configurable: true, + get() { + throw new DOMException( + "Failed to read the 'localStorage' property from 'Window': Access is denied for this document.", + "SecurityError", + ); + }, + }); +} + +function restoreSiteStorageAccess(): void { + if (originalLocalStorageDescriptor) { + Object.defineProperty(window, "localStorage", originalLocalStorageDescriptor); + } else { + // No own descriptor to restore (storage inherited from the prototype): + // deleting the override re-exposes the inherited accessor for later tests. + Reflect.deleteProperty(window, "localStorage"); + } +} + +async function importClerkAuthenticationModule() { + return import("./clerkAuthentication.svelte"); +} + +async function importToastStoreModule() { + return import("../stores/toastStore"); +} + +beforeEach(() => { + vi.resetModules(); +}); + +afterEach(() => { + restoreSiteStorageAccess(); + vi.unstubAllEnvs(); + vi.restoreAllMocks(); +}); + +describe("loadClerkAuthentication", () => { + it("disables auth quietly when the browser denies site storage access", async () => { + vi.stubEnv("VITE_CLERK_PUBLISHABLE_KEY", "pk_test_storage-gate"); + const consoleInfoSpy = vi.spyOn(console, "info").mockImplementation(() => {}); + denySiteStorageAccess(); + const { loadClerkAuthentication, clerkAuthentication } = + await importClerkAuthenticationModule(); + const { toasts } = await importToastStoreModule(); + + await expect(loadClerkAuthentication()).resolves.toBeUndefined(); + + expect(clerkAuthentication.isLoaded).toBe(false); + expect(get(toasts)).toEqual([]); + expect(consoleInfoSpy).toHaveBeenCalledWith( + expect.stringContaining("denies site storage access"), + expect.any(DOMException), + ); + }); + + it("disables auth quietly when the build has no publishable key", async () => { + vi.stubEnv("VITE_CLERK_PUBLISHABLE_KEY", ""); + const consoleInfoSpy = vi.spyOn(console, "info").mockImplementation(() => {}); + const { loadClerkAuthentication, clerkAuthentication } = + await importClerkAuthenticationModule(); + const { toasts } = await importToastStoreModule(); + + await expect(loadClerkAuthentication()).resolves.toBeUndefined(); + + expect(clerkAuthentication.isLoaded).toBe(false); + expect(get(toasts)).toEqual([]); + expect(consoleInfoSpy).toHaveBeenCalledWith( + expect.stringContaining("no VITE_CLERK_PUBLISHABLE_KEY"), + ); + }); +}); diff --git a/frontend/src/lib/composables/clerkAuthentication.svelte.ts b/frontend/src/lib/composables/clerkAuthentication.svelte.ts index a03e09a3..d6a47e22 100644 --- a/frontend/src/lib/composables/clerkAuthentication.svelte.ts +++ b/frontend/src/lib/composables/clerkAuthentication.svelte.ts @@ -30,13 +30,38 @@ export const clerkAuthentication = new ClerkAuthenticationState(); let clerkClient: Clerk | null = null; +/** Key read by the storage probe; never persisted, so any name outside real keys works. */ +const STORAGE_ACCESS_PROBE_KEY = "java-chat-storage-access-probe"; + +/** Outcome of the site-storage probe; `denial` is the error the browser threw. */ +type SiteStorageAccess = { accessible: true } | { accessible: false; denial: unknown }; + +/** + * Probes whether this browser grants the page access to site storage. + * Kiosk and hardened-privacy browsers (e.g. e-ink display frames with "block + * site data" enabled) make the `window.localStorage` getter itself throw a + * SecurityError; `@clerk/clerk-js` reads it unguarded during `Clerk.load()` + * and cannot keep a session without it, so sign-in is impossible there. + */ +function probeSiteStorageAccess(): SiteStorageAccess { + try { + window.localStorage.getItem(STORAGE_ACCESS_PROBE_KEY); + return { accessible: true }; + } catch (storageAccessDenial) { + return { accessible: false, denial: storageAccessDenial }; + } +} + /** * Loads Clerk exactly once and starts mirroring its session into * {@link clerkAuthentication}. Failures surface as an error toast and a * rethrown error — the chat itself works unauthenticated, but a broken auth - * configuration must never be silent ([RC1f]). + * configuration must never be silent ([RC1f]). Environments where auth cannot + * exist (no publishable key in the build, or a browser that denies site + * storage) are deliberate disabled states, not failures: the function returns + * quietly and auth controls stay hidden. * - * @throws Error when the publishable key is absent or `Clerk.load()` fails. + * @throws Error when `Clerk.load()` rejects (misconfigured key, network or SDK failure). */ export async function loadClerkAuthentication(): Promise { if (clerkClient) { @@ -50,6 +75,18 @@ export async function loadClerkAuthentication(): Promise { console.info("Clerk authentication disabled: no VITE_CLERK_PUBLISHABLE_KEY in this build."); return; } + const siteStorageAccess = probeSiteStorageAccess(); + if (!siteStorageAccess.accessible) { + // Environment gate, not an error: sign-in cannot work where the browser + // denies site storage, so auth controls stay hidden — same deliberate + // disabled state as a build without a publishable key. Skipping here also + // avoids downloading the SDK chunks on such devices. + console.info( + "Clerk authentication disabled: this browser denies site storage access.", + siteStorageAccess.denial, + ); + return; + } // Dynamic imports keep the ~700 kB Clerk SDK out of the first-paint chunk; // auth controls appear once loaded, the chat itself never waits on them. // The npm ESM build of clerk-js ships without prebuilt components, so the diff --git a/metadata/RELEASE_READINESS.md b/metadata/RELEASE_READINESS.md new file mode 100644 index 00000000..a5b8a096 --- /dev/null +++ b/metadata/RELEASE_READINESS.md @@ -0,0 +1,50 @@ +# App Store release readiness + +The customer-facing copy, screenshots, processed builds, and privacy declaration are complete for iOS and Mac version 1.0. Both platform versions have been submitted to App Review. + +## Completed + +- App name: `Java Chat - AI Learning` +- Subtitle: `Cited answers. Guided lessons.` +- Description, keywords, promotional text, marketing URL, support URL, and privacy-policy URL +- Five iPhone screenshots at 1320 × 2868 +- Five iPad screenshots at 2064 × 2752 +- Three Mac screenshots at 2880 × 1800 +- iOS build 2 attached to version 1.0 +- Mac build 1 attached to version 1.0 +- Copyright: `2026 William Callahan` +- Primary category: `Education` +- Secondary category: `Developer Tools` +- Price schedule: Free, with the United States as the base territory +- Age-rating declaration: no objectionable or sensitive content; all questionnaire fields are `NONE` or `false` +- App Review contact: William Callahan, `william@javachat.ai`, `+1 650-638-8380` +- App Review notes for both iOS and Mac; no demo account is required +- Content-rights declaration: uses third-party content, matching the app's cited and linked documentation +- Availability: 123 non-European storefronts enabled; 52 European, EU, Europe-boundary, and UK-associated storefronts disabled +- Automatic availability in future storefronts: disabled, so a new European storefront cannot be enabled implicitly +- App Privacy declaration: published by William Callahan +- App Privacy data types: Name, Email Address, Customer Support, Other User Content, Search History, User ID, Product Interaction, Other Usage Data, Other Diagnostic Data, and Other Data Types +- App Privacy use: account and support fields are used for App Functionality; content, identifiers, usage, diagnostics, and other technical data are used for App Functionality and Analytics; all declared data is conservatively linked to the user and none is used for tracking +- Free Apps Agreement: active for all countries and regions through July 30, 2027 +- Paid Apps Agreement: unsigned and not required while Java Chat remains free + +The excluded storefront codes are `AIA`, `ALB`, `ARM`, `AUT`, `AZE`, `BEL`, `BGR`, `BIH`, `BLR`, `BMU`, `CHE`, `CYM`, `CYP`, `CZE`, `DEU`, `DNK`, `ESP`, `EST`, `FIN`, `FRA`, `GBR`, `GEO`, `GRC`, `HRV`, `HUN`, `IRL`, `ISL`, `ITA`, `KAZ`, `LTU`, `LUX`, `LVA`, `MDA`, `MKD`, `MLT`, `MNE`, `MSR`, `NLD`, `NOR`, `POL`, `PRT`, `ROU`, `RUS`, `SRB`, `SVK`, `SVN`, `SWE`, `TCA`, `TUR`, `UKR`, `VGB`, and `XKS`. This conservative set includes transcontinental Europe-boundary countries and the six separately listed UK Overseas Territories. + +Apple does not permit a `What’s New` field on a first release. The drafted version 1.0 notes are retained under `metadata/release-notes/1.0/en-US.md` for future reuse. + +## Validation + +- iOS 1.0: zero validation errors, zero warnings, and one nonblocking public-API advisory whose App Privacy state was confirmed published in the authenticated web interface +- Mac 1.0: zero validation errors, zero warnings, and one nonblocking public-API advisory whose App Privacy state was confirmed published in the authenticated web interface +- iOS build 2: attached, `VALID`, unexpired, encryption exempt, with an extracted App Store icon +- Mac build 1: attached, `VALID`, unexpired, encryption exempt, with an extracted App Store icon + +## Submission state + +- iOS 1.0: `WAITING_FOR_REVIEW`, submission `b69dada6-a287-41fd-8184-125bad73c73c`, submitted August 2, 2026 at 5:29:57 PM PDT +- Mac 1.0: `WAITING_FOR_REVIEW`, submission `94e9a04c-af4d-4e9d-b64a-4329ebfe6aa1`, submitted August 2, 2026 at 5:31:07 PM PDT +- Release type: automatic after App Review approval + +## Suggested App Review notes + +Java Chat is an internet-connected Java learning app. The core review path can be exercised without signing in: open Chat to ask a Java question and inspect cited sources, or open Learn to browse a guided lesson and its lesson-focused chat. The iPhone, iPad, and Mac apps present the same JavaChat.ai learning experience in their platform WebKit shells. diff --git a/metadata/app-info/en-US.json b/metadata/app-info/en-US.json new file mode 100644 index 00000000..dbe44088 --- /dev/null +++ b/metadata/app-info/en-US.json @@ -0,0 +1,5 @@ +{ + "name": "Java Chat - AI Learning", + "subtitle": "Cited answers. Guided lessons.", + "privacyPolicyUrl": "https://javachat.ai/privacy" +} diff --git a/metadata/release-notes/1.0/en-US.md b/metadata/release-notes/1.0/en-US.md new file mode 100644 index 00000000..7edb7227 --- /dev/null +++ b/metadata/release-notes/1.0/en-US.md @@ -0,0 +1,10 @@ +Welcome to Java Chat on iPhone, iPad, and Mac. + +Version 1.0 includes: + +- Streaming AI-assisted answers to Java and JVM questions +- Expandable source links on cited responses +- Guided lessons from Java fundamentals to modern JVM languages and frameworks +- Lesson-focused follow-up chat +- Formatted code and quick copy controls +- System, Light, and Dark appearance options diff --git a/metadata/version/1.0/en-US.json b/metadata/version/1.0/en-US.json new file mode 100644 index 00000000..46cb6411 --- /dev/null +++ b/metadata/version/1.0/en-US.json @@ -0,0 +1,7 @@ +{ + "description": "Java Chat is an AI-assisted learning companion for Java and the wider JVM ecosystem. Ask about APIs, language features, patterns, and best practices, then read the answer as it streams and open its cited sources.\n\nBUILD UNDERSTANDING, ONE QUESTION AT A TIME\n\nStart with your own question or tap a suggested topic. Java Chat formats code clearly, keeps the conversation moving, and makes source links easy to explore.\n\nLEARN WITH A STRUCTURED PATH\n\nBrowse guided lessons that start with Java fundamentals—variables, loops, methods, classes, collections, and testing—and continue into records, streams, pattern matching, virtual threads, modules, and memory.\n\nGO BEYOND THE JAVA LANGUAGE\n\nExplore lessons on Kotlin, Scala, Groovy, Clojure, Spring Boot, and Quarkus. Each lesson includes focused content and its own chat, so your follow-up questions stay with the topic.\n\nDESIGNED FOR FOCUSED STUDY\n\n• Streaming answers for Java and JVM questions\n• Expandable source links on cited responses\n• Guided lessons from fundamentals to modern topics\n• Lesson-focused follow-up chat\n• Formatted code with syntax highlighting\n• Quick copy controls for answers\n• System, Light, and Dark appearance options\n\nWhether you are learning the basics or revisiting modern JVM concepts, Java Chat brings questions, explanations, sources, and guided practice together across iPhone, iPad, and Mac.\n\nAn internet connection is required.", + "keywords": "programming,coding,JDK,JVM,tutor,Spring,Boot,Kotlin,Quarkus,Scala,Groovy,Clojure,records,threads,API", + "marketingUrl": "https://javachat.ai", + "promotionalText": "Ask a Java question, follow the answer as it streams, and open cited sources. Or choose a guided lesson, then ask follow-up questions in that lesson’s context.", + "supportUrl": "https://javachat.ai/contact" +} diff --git a/screenshots/captions/en-US.json b/screenshots/captions/en-US.json new file mode 100644 index 00000000..70384a6f --- /dev/null +++ b/screenshots/captions/en-US.json @@ -0,0 +1,60 @@ +{ + "iphone": [ + { + "headline": "Ask Anything About Java", + "subtitle": "Start with a question or prompt" + }, + { + "headline": "Read Answers Clearly", + "subtitle": "Formatted code stays easy to scan" + }, + { + "headline": "Learn From Real Code", + "subtitle": "See Java concepts in context" + }, + { + "headline": "Choose Your Next Lesson", + "subtitle": "Tap a focused topic to begin" + }, + { + "headline": "Ask as You Learn", + "subtitle": "Ask follow-ups inside each lesson" + } + ], + "ipad": [ + { + "headline": "Ask Anything About Java", + "subtitle": "Turn the big screen into a study space" + }, + { + "headline": "Read Answers Clearly", + "subtitle": "Give explanations and code more room" + }, + { + "headline": "Ask as You Learn", + "subtitle": "Keep follow-ups tied to each lesson" + }, + { + "headline": "Choose Your Next Lesson", + "subtitle": "Browse the lesson catalog at a glance" + }, + { + "headline": "Study Side by Side", + "subtitle": "Read a lesson with its focused chat" + } + ], + "mac": [ + { + "headline": "Ask Anything About Java", + "subtitle": "Start a focused Java session on Mac" + }, + { + "headline": "Read Answers Clearly", + "subtitle": "Read formatted code in a focused workspace" + }, + { + "headline": "Choose Your Next Lesson", + "subtitle": "Browse Java and JVM topics at a glance" + } + ] +} diff --git a/screenshots/final/en-US/ipad/01-ask-anything.png b/screenshots/final/en-US/ipad/01-ask-anything.png new file mode 100644 index 00000000..8393e1e3 Binary files /dev/null and b/screenshots/final/en-US/ipad/01-ask-anything.png differ diff --git a/screenshots/final/en-US/ipad/02-read-answers.png b/screenshots/final/en-US/ipad/02-read-answers.png new file mode 100644 index 00000000..686a911e Binary files /dev/null and b/screenshots/final/en-US/ipad/02-read-answers.png differ diff --git a/screenshots/final/en-US/ipad/03-open-sources.png b/screenshots/final/en-US/ipad/03-open-sources.png new file mode 100644 index 00000000..272a8c89 Binary files /dev/null and b/screenshots/final/en-US/ipad/03-open-sources.png differ diff --git a/screenshots/final/en-US/ipad/04-choose-lesson.png b/screenshots/final/en-US/ipad/04-choose-lesson.png new file mode 100644 index 00000000..eb9630db Binary files /dev/null and b/screenshots/final/en-US/ipad/04-choose-lesson.png differ diff --git a/screenshots/final/en-US/ipad/05-ask-as-you-learn.png b/screenshots/final/en-US/ipad/05-ask-as-you-learn.png new file mode 100644 index 00000000..f8eb2602 Binary files /dev/null and b/screenshots/final/en-US/ipad/05-ask-as-you-learn.png differ diff --git a/screenshots/final/en-US/iphone/01-ask-anything.png b/screenshots/final/en-US/iphone/01-ask-anything.png new file mode 100644 index 00000000..7b8bfd36 Binary files /dev/null and b/screenshots/final/en-US/iphone/01-ask-anything.png differ diff --git a/screenshots/final/en-US/iphone/02-read-answers.png b/screenshots/final/en-US/iphone/02-read-answers.png new file mode 100644 index 00000000..227fe4a3 Binary files /dev/null and b/screenshots/final/en-US/iphone/02-read-answers.png differ diff --git a/screenshots/final/en-US/iphone/03-learn-from-code.png b/screenshots/final/en-US/iphone/03-learn-from-code.png new file mode 100644 index 00000000..e42d6c7f Binary files /dev/null and b/screenshots/final/en-US/iphone/03-learn-from-code.png differ diff --git a/screenshots/final/en-US/iphone/04-choose-lesson.png b/screenshots/final/en-US/iphone/04-choose-lesson.png new file mode 100644 index 00000000..4cbd3b48 Binary files /dev/null and b/screenshots/final/en-US/iphone/04-choose-lesson.png differ diff --git a/screenshots/final/en-US/iphone/05-ask-as-you-learn.png b/screenshots/final/en-US/iphone/05-ask-as-you-learn.png new file mode 100644 index 00000000..3f0a06f2 Binary files /dev/null and b/screenshots/final/en-US/iphone/05-ask-as-you-learn.png differ diff --git a/screenshots/final/en-US/mac/01-ask-anything.png b/screenshots/final/en-US/mac/01-ask-anything.png new file mode 100644 index 00000000..da098c5f Binary files /dev/null and b/screenshots/final/en-US/mac/01-ask-anything.png differ diff --git a/screenshots/final/en-US/mac/02-read-answers.png b/screenshots/final/en-US/mac/02-read-answers.png new file mode 100644 index 00000000..433668a2 Binary files /dev/null and b/screenshots/final/en-US/mac/02-read-answers.png differ diff --git a/screenshots/final/en-US/mac/03-choose-lesson.png b/screenshots/final/en-US/mac/03-choose-lesson.png new file mode 100644 index 00000000..90510732 Binary files /dev/null and b/screenshots/final/en-US/mac/03-choose-lesson.png differ