Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
50e0c18
fix: publish Play products to every region instead of the US only
hyochan Aug 5, 2026
b35b72a
fix: stop rejecting fresh Play purchases before they can be acknowledged
hyochan Aug 5, 2026
21c5532
fix: route MCP sessions to their owning Fly machine and 404 lost sess…
hyochan Aug 5, 2026
a6f069e
feat: add localized store listings, and apply self-review findings
hyochan Aug 6, 2026
61451ed
fix: address review feedback on localized listings
hyochan Aug 6, 2026
d18acbb
fix: close localization round-trip and validation gaps found in self-…
hyochan Aug 6, 2026
1ce4120
fix: restore US-first pull ranking and compare every locale before sk…
hyochan Aug 6, 2026
48277f4
fix: stop localizations leaking between products and hide no dry-run …
hyochan Aug 6, 2026
79a8353
fix: make the new mcp-server CI step actually check formatting
hyochan Aug 6, 2026
88af463
feat: let a product name its sales regions, and align the write's reg…
hyochan Aug 6, 2026
d39614f
fix: close the round-three findings, including two fixes that had no …
hyochan Aug 6, 2026
81bef3b
fix: guard the dashboard prefill instead of the clear
hyochan Aug 6, 2026
39eef5f
fix: close round-four findings, and stop regions being a phantom
hyochan Aug 6, 2026
b38f3a8
fix: hide sales regions where they cannot apply instead of failing on…
hyochan Aug 6, 2026
de0675e
fix: replace the inferred prefill with an explicit load, and fix stal…
hyochan Aug 6, 2026
cafb74c
fix: validate sales regions as real territories, and put the dashboar…
hyochan Aug 6, 2026
79356ab
fix: undo a round-five change that killed sales regions outright
hyochan Aug 6, 2026
9b276b0
revert: drop the sales-regions field from this PR
hyochan Aug 6, 2026
80b4028
fix: close a replay hole the #289 fix opened, and cover the paths tha…
hyochan Aug 6, 2026
8f67a54
test: cover the localization, retry, and legacy-fallback contracts
hyochan Aug 6, 2026
9f8bcdc
feat: let a product declare the regions it is sold in
hyochan Aug 6, 2026
2691822
refactor: stop writing webhook idempotency keys (#241 phase 2)
hyochan Aug 6, 2026
c69a493
fix: harden product sync and receipt replay
hyochan Aug 6, 2026
b13e301
test: bind selected price to base plan
hyochan Aug 6, 2026
1ec3072
fix: repair webhook retries and region guard
hyochan Aug 6, 2026
176ed85
fix(kit): harden webhook and ASC sync
hyochan Aug 7, 2026
4faeb26
fix(kit): order equal-time webhook events
hyochan Aug 7, 2026
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
14 changes: 14 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@
"runtimeExecutable": "bun",
"runtimeArgs": ["run", "--cwd", "packages/docs", "dev"],
"port": 5173
},
{
"name": "kit-dashboard",
"runtimeExecutable": "bun",
"runtimeArgs": [
"run",
"--cwd",
"packages/kit",
"vite",
"--port",
"5173",
"--strictPort"
],
"port": 5173
}
]
}
14 changes: 14 additions & 0 deletions .github/workflows/deploy-kit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ on:
branches: [main]
paths:
- "packages/kit/**"
# kit.openiap.dev/mcp is served by kit's Fly binary importing
# @hyodotdev/openiap-mcp-server/web straight from source, so an
# MCP-server change must redeploy kit or it never ships (issue #287).
- "packages/mcp-server/**"
- ".github/workflows/deploy-kit.yml"
- "bun.lock"
- "package.json"
pull_request:
paths:
- "packages/kit/**"
- "packages/mcp-server/**"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- ".github/workflows/deploy-kit.yml"
- "bun.lock"
- "package.json"
Expand Down Expand Up @@ -56,6 +61,15 @@ jobs:
- name: Run tests (convex + server unit tests)
run: bun run test

- name: Lint + test MCP server (served by kit's /mcp route)
# kit's Fly binary imports @hyodotdev/openiap-mcp-server/web from
# source, so its regressions ship with kit deploys. This workflow
# is the only CI that runs the MCP server's own suite.
working-directory: packages/mcp-server
run: |
bun run lint
bun run test

- name: Vite build
env:
VITE_KIT_CONVEX_URL: https://placeholder-build-1.convex.cloud
Expand Down
13 changes: 12 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ node scripts/audit-non-godot-parity.mjs
# Cost: roughly 30-60s on first run after a clean checkout, ~15-20s on
# warm checkouts (lint + tests + smoke). If you really need to bypass,
# fix the underlying issue rather than passing --no-verify.
if git diff --cached --name-only --diff-filter=ACMR | grep -q '^packages/kit/'; then
# `packages/mcp-server` is compiled into kit's Fly binary and served at
# kit.openiap.dev/mcp, so its regressions ship with kit. deploy-kit.yml
# triggers on both paths and runs both suites; mirror that here or a
# commit touching only the MCP server would skip the gate entirely.
if git diff --cached --name-only --diff-filter=ACMR \
| grep -qE '^packages/(kit|mcp-server)/'; then
echo "🧰 kit-touched commit — running CI-equivalent gate…"

# Lockfile must satisfy package.json. Without --frozen-lockfile,
Expand Down Expand Up @@ -103,6 +108,12 @@ if git diff --cached --name-only --diff-filter=ACMR | grep -q '^packages/kit/';
# conflicts, missing dist/index.html, server.ts import order issues.
echo "→ kit smoke (compile + boot probe)"
bun run --filter @hyodotdev/openiap-kit smoke:server

# MCP server ships inside the same binary; its suite is the only
# coverage for the /mcp session-routing behaviour.
echo "→ mcp-server lint + tests"
bun run --filter @hyodotdev/openiap-mcp-server lint
bun run --filter @hyodotdev/openiap-mcp-server test
fi

# Paths-aware Flutter analyze. Triggers on any libraries/flutter_inapp_purchase
Expand Down
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/kit/convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ import type * as products_asc from "../products/asc.js";
import type * as products_ascReview from "../products/ascReview.js";
import type * as products_jobs from "../products/jobs.js";
import type * as products_jwt from "../products/jwt.js";
import type * as products_localizations from "../products/localizations.js";
import type * as products_mutation from "../products/mutation.js";
import type * as products_play from "../products/play.js";
import type * as products_query from "../products/query.js";
import type * as products_regions from "../products/regions.js";
import type * as products_sync from "../products/sync.js";
import type * as products_syncResult from "../products/syncResult.js";
import type * as projects_helpers from "../projects/helpers.js";
Expand Down Expand Up @@ -120,9 +122,11 @@ declare const fullApi: ApiFromModules<{
"products/ascReview": typeof products_ascReview;
"products/jobs": typeof products_jobs;
"products/jwt": typeof products_jwt;
"products/localizations": typeof products_localizations;
"products/mutation": typeof products_mutation;
"products/play": typeof products_play;
"products/query": typeof products_query;
"products/regions": typeof products_regions;
"products/sync": typeof products_sync;
"products/syncResult": typeof products_syncResult;
"projects/helpers": typeof projects_helpers;
Expand Down
15 changes: 13 additions & 2 deletions packages/kit/convex/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,13 @@ export const backfillPurchaseStatsFromPurchases = migrations.define({
const hasOrderId =
typeof doc.orderId === "string" && doc.orderId.length > 0
? true
: extractOrderIdFromRemoteResponse(doc.store, doc.remoteResponse) !==
null;
: extractOrderIdFromRemoteResponse(
doc.store,
doc.remoteResponse,
doc.requestData.store === "google"
? doc.requestData.expectedProductId
: undefined,
) !== null;

await applyPurchaseStatsDelta(
ctx,
Expand Down Expand Up @@ -197,6 +202,9 @@ export const backfillPurchaseProductIds = migrations.define({
const productId = extractProductIdFromRemoteResponse(
doc.store,
doc.remoteResponse,
doc.requestData.store === "google"
? doc.requestData.expectedProductId
: undefined,
);

if (productId === null) {
Expand Down Expand Up @@ -242,6 +250,9 @@ export const backfillPurchaseOrderIds = migrations.define({
const orderId = extractOrderIdFromRemoteResponse(
doc.store,
doc.remoteResponse,
doc.requestData.store === "google"
? doc.requestData.expectedProductId
: undefined,
);

if (orderId === null) {
Expand Down
117 changes: 117 additions & 0 deletions packages/kit/convex/products/asc.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { describe, expect, it, vi } from "vitest";

import {
ProductSyncCancelledError,
ascPriceStartAttributes,
pushAscReviewLocalizations,
syncAscReviewLocalization,
ascCustomerPriceToMicros,
createAscReviewEligibilityLoader,
getAscReviewFinalizeDisposition,
Expand Down Expand Up @@ -542,6 +546,18 @@ describe("mapAscOfferKind", () => {
});
});

describe("ascPriceStartAttributes", () => {
it("omits startDate for an immediately effective IAP price", () => {
expect(ascPriceStartAttributes()).toEqual({});
});

it("keeps an explicitly scheduled startDate", () => {
expect(ascPriceStartAttributes("2026-08-08")).toEqual({
startDate: "2026-08-08",
});
});
});

describe("pickActivePriceRow", () => {
const today = new Date().toISOString().slice(0, 10);
const yesterday = new Date(Date.now() - 86_400_000)
Expand Down Expand Up @@ -689,3 +705,104 @@ describe("parseIntroOffers", () => {
expect(out).toEqual([]);
});
});

describe("pushAscReviewLocalizations", () => {
const listings = [
{ locale: "en-US", title: "Coins", description: "100 coins" },
{ locale: "ko-KR", title: "코인" },
{ locale: "ja-JP", title: "コイン" },
];

it("writes every locale, not just the base listing", async () => {
const seen: string[] = [];
await pushAscReviewLocalizations({
listings,
productId: "coins",
upsert: async (l) => {
seen.push(l.locale);
},
recordFailure: () => {
throw new Error("unexpected failure");
},
});
expect(seen).toEqual(["en-US", "ko-KR", "ja-JP"]);
});

it("keeps going after one locale fails, and names it", async () => {
const seen: string[] = [];
const failures: Array<{ productId: string; reason: string }> = [];
await pushAscReviewLocalizations({
listings,
productId: "coins",
upsert: async (l) => {
seen.push(l.locale);
if (l.locale === "ko-KR") throw new Error("ASC rejected it");
},
recordFailure: (f) => failures.push(f),
});
// ja-JP must still be attempted.
expect(seen).toEqual(["en-US", "ko-KR", "ja-JP"]);
expect(failures).toEqual([
{ productId: "coins (localization ko-KR)", reason: "ASC rejected it" },
]);
});

it("propagates a base-listing failure so the row fails", async () => {
await expect(
pushAscReviewLocalizations({
listings,
productId: "coins",
upsert: async () => {
throw new Error("base blew up");
},
recordFailure: () => undefined,
}),
).rejects.toThrow("base blew up");
});

it("lets a cancellation keep unwinding instead of grinding on", async () => {
const seen: string[] = [];
await expect(
pushAscReviewLocalizations({
listings,
productId: "coins",
upsert: async (l) => {
seen.push(l.locale);
if (l.locale === "ko-KR") throw new ProductSyncCancelledError();
},
recordFailure: () => {
throw new Error("cancellation must not be recorded as a failure");
},
}),
).rejects.toBeInstanceOf(ProductSyncCancelledError);
expect(seen).toEqual(["en-US", "ko-KR"]);
});

it("propagates cancellation through the outer review sync boundary", async () => {
const seen: string[] = [];
const recordFailure = vi.fn();

await expect(
syncAscReviewLocalization({
reviewVersion: {
versionId: "version-1",
alreadySubmitted: false,
attachedToSubmission: false,
},
listings,
productId: "coins",
findMismatchedLocale: vi.fn(async () => undefined),
upsert: async (listing) => {
seen.push(listing.locale);
if (listing.locale === "ko-KR") {
throw new ProductSyncCancelledError();
}
},
recordFailure,
}),
).rejects.toBeInstanceOf(ProductSyncCancelledError);

expect(seen).toEqual(["en-US", "ko-KR"]);
expect(recordFailure).not.toHaveBeenCalled();
});
});
Loading