references/checks.md
# Checklists: A (status check) and B (pre-release check)
For both lists, **read only. Do not edit files.**
## List A: status check (doctor)
Use exactly four verdicts: **present / missing / not applicable / needs confirmation**.
- **present**: you confirmed it at a file and line
- **missing**: you checked where it should be, and it is not there
- **not applicable**: this game does not need it (give the reason in one line)
- **needs confirmation**: the code cannot tell (server settings, ONE store registration, etc.). Say what to look at
The verdict says whether the thing named in the item exists. A14, A15, and A19 name things that
must **not** be there, so for them **missing** is the good result and **present** is a problem to fix.
Do not invent other verdicts such as "passed".
Show the full table; the point is to show what is already done too. Cite evidence as `path:line`.
| # | Item | What to check |
|---|---|---|
| A1 | SDK load | The SDK is imported in a `<script type="module">` and **runs before the game scripts** (a game script loaded with a plain `<script src>` runs first even if its tag comes later). The URL is the official CDN, `https://h5sdk.onestore.net/lib/{{version}}/…` |
| A2 | Entry point | `createSDK()` is called (or `window.ONEstoreH5` is used) |
| A3 | Call order | `initializeAsync` → `setLoadingProgress` → `startGameAsync`, in order and chained |
| A4 | Unsupported environment | The `err` in the `initializeAsync` result is checked (it resolves, it does not reject) |
| A5 | Feature checks | `sdk.ads.isSupported()` / `sdk.iap.isSupported()` before showing ad or purchase UI, called **after init** |
| A6 | Save on exit | An `on("exit")` listener exists, the save looks synchronous and within 0.5 seconds, and `quit()` is not called again |
| A7 | Save before quit | If the game calls `quit()` itself, it saves before the call (`exit` does not fire on that path) |
| A8 | Back key | `onBackPressed` is registered and is a **synchronous function** (no async) |
| A9 | pause / resume | Listeners exist, handling is idempotent, `ringerSilent` from `resume` is applied |
| A10 | Rewarded ads | `requestId` is passed to `showRewardedAsync`, and the reward branch is `status === "rewarded"` only |
| A10-1 | Ad preload | `loadRewarded` / `loadInterstitial` is called early, when the game starts (recommended; without it the first ad takes longer) |
| A11 | Reward granting | Whether the client grants currency directly (if so, needs confirmation: granting through the server postback is the policy) |
| A12 | Purchase | `reason` branches for `purchase` (`user_cancelled`, etc.) and the hook for server-side `purchaseToken` verification |
| A12-1 | developerPayload | `purchase` requests include `developerPayload`, **the slot for the information needed to grant the item (such as a user identifier).** It is optional, but without it the purchase result and PNS notification cannot be matched for granting. Also check that it is at most 200 bytes in UTF-8 |
| A13 | External navigation | `openDeeplink` is used instead of `window.top.location` or `window.open` |
| A14 | Frame-busting code | Frame-busting code such as `window.parent !== window` checks (should be **missing**) |
| A15 | Forbidden APIs | Use of `alert` / `confirm` / `prompt` / `print` (should be **missing**) |
| A16 | Viewport | `100dvh`, `overscroll-behavior: none`, `viewport-fit=cover`, and safeArea are applied |
| A17 | Frame-allow headers | No `X-Frame-Options`, and `frame-ancestors` is set. These are server settings, so if the code does not show them: **needs confirmation** |
| A18 | Cookies and storage | If the session depends on cookies, `SameSite=None; Secure`; saves are keyed by `playerId` |
| A19 | Debug leftovers | A `setDebugLogger` call left in the release code (should be **missing**) |
| A20 | Listener timing | `pause` / `resume` / `exit` listeners are registered before `initializeAsync()` (recommended; events can arrive right after init) |
| A21 | Calls into the game | When SDK code and game code are separate scripts, SDK code calls the game object only after it exists (waits with `whenGame` from SKILL.md or an equivalent). The `initializeAsync()` result can arrive before the game script runs |
If items that need values (placementId, productId, SDK URL, how requestId is issued) are empty, give
the fill-in form from SKILL.md under the table.
## List B: pre-release check
**List only what needs fixing.** Show passed items as a count. Order: blockers → non-blockers →
needs confirmation → passed count.
### Blockers: shipping as is causes incidents
| # | Item | Why it blocks |
|---|---|---|
| B1 | Rewards granted from the client-side `rewarded` alone | Reproducible abuse. The grant is confirmed through SSV → verification → server postback |
| B2 | Items granted without server-side `purchaseToken` verification | No protection against forged payments. Granting and consumption are the server's job, and the payment notification (PNS) also goes to the server |
| B3 | Frame-busting code left in | The game does not load at all |
| B4 | An `X-Frame-Options: DENY/SAMEORIGIN` response, or `frame-ancestors` without both `https://*.onestore.co.kr` and `https://*.onestore.net` | The game does not load, or **registration review rejects it** (server setting; if the code does not show it, needs confirmation) |
| B5 | Served over HTTP, or redirected to another origin | Blocked as mixed content, or may fail to load or communicate |
| B6 | `requestId` missing for rewarded ads | Every ad fails with `invalid_request` |
| B7 | The SDK is loaded from somewhere other than the official CDN (`https://h5sdk.onestore.net/lib/{{version}}/…`) | Depends on a self-hosted or unofficial copy. Replace it with the official URL |
| B8 | SDK code calls the game object before the game script has run | The game may never start: always in a regular browser, and in the app depending on timing |
### Non-blockers: works, but risky
| # | Item | Result |
|---|---|---|
| B9 | No save in `on("exit")` | Progress is lost on exit and restart |
| B10 | The exit save is async or takes over 0.5 seconds | The save is cut off (the app closes the screen within that time) |
| B11 | `onBackPressed` not registered, or async | The exit popup on every back press, or answers over 200 ms count as not consumed |
| B12 | Rewards given on `dismissed` or `no_fill` | Violates the policy; reward only on `rewarded` |
| B13 | `user_cancelled` shown as an error | An error popup after the user cancelled. Causes support tickets |
| B14 | `ringerSilent` ignored | Sound in silent mode. Causes support tickets |
| B15 | `alert` / `confirm` / `prompt` used | Does not work or is blocked in the iframe |
| B16 | safeArea not applied | UI hidden behind the system bars |
| B17 | The session depends on cookies or storage affected by partitioning | Sign-in and saves are lost intermittently |
### Needs confirmation: outside the code
- ONE store registration: test URL, ad placements (placementId), in-app products (productId)
- Postback endpoint registration and **idempotent handling** (postbacks can be resent)
- Frame-allow headers from the real server (`curl -sI <game URL> | grep -i -e x-frame -e content-security`)
references/diagnose.md
# Diagnosis from symptoms: "it doesn't work and I don't know why"
Do not start reading code with only a symptom. The cause is often outside the code (server headers,
ONE store registration, app version). **First narrow down where it stops, and ask about what the
code cannot tell.**
## Collect first
| Tool | What it shows |
|---|---|
| `sdk.setDebugLogger(msg => console.log("[H5SDK]", msg))` | The SDK's internal decisions. **The first tool for integration debugging.** It shows how far init got |
| Browser or WebView console | Frame blocking (`Refused to frame`), mixed content, and module load failures all show up here. On a device, open the WebView console with `chrome://inspect#devices` (only if the app build allows WebView debugging) |
| `sdk.getState()` | Which stage it stopped at. Check for an unsupported environment with `err` in the `initializeAsync()` result |
| `curl -sI <game URL>` | `X-Frame-Options`, `Content-Security-Policy`, redirects (3xx): server settings the code does not show |
## Symptom → how to narrow it down
### The screen is blank (the game does not appear at all)
Most likely not a code problem. Check in this order.
1. Whether `curl -sI` shows `X-Frame-Options`. `DENY` / `SAMEORIGIN` is the cause
2. Whether the game URL is HTTP, or redirects from HTTPS to another origin. That is the cause
3. `Refused to frame` or a CSP violation in the console means `frame-ancestors` is not set correctly
4. Whether frame-busting code (such as `window.parent !== window` checks) remains. Remove it
5. If none of these, get the first error in the console and decide from there
### `initializeAsync` returns `err`
Not an error: **the environment is unsupported** (it resolves, it does not reject).
- Opened in a regular browser → expected. Test in the ONE store app's web game test menu
(`sdk-context.md`, "Device testing")
- `err` inside the app → check the SDK load order (later than the game scripts), or whether the SDK
is called inside an iframe the game created. Initialization fails inside such an iframe
### `initializeAsync` rejects (`timeout`, etc.)
The app did not respond. Check the SDK load timing first (top level of the game document, before
the game code). If that is correct, it is an app or hosting problem: ask for the reproduction
environment (app version, device) and pass it to ONE store.
### Boot stops with `… is not a function` (the game does not start)
Check two things first.
1. **Whether a global has the same name as an element `id`.** If they match, like
`<canvas id="game">` and `window.game`, then `window.game` is that element until the game code
overwrites it. Rename the global
2. **Execution order.** The SDK is `type="module"` (runs after parsing), so a game script loaded with
a plain `<script>` runs first. Conversely, if the game script was changed to a module, the SDK
boot code may run before the game object exists. Check that it waits until the object is ready
### `sdk_not_initialized`
A call order violation: `startGameAsync()` or similar was called **before** `initializeAsync()`
completed. Find where the Promise chain breaks (calls made without `await` / `then`).
### `isSupported()` is false
- First check that it is called **after** `initializeAsync()`. Before that there is nothing to decide from
- False after init → the app version does not provide the feature. Code cannot fix it. Hiding the
feature's UI is the right handling. For product lookup, the app must be v8.5.0 or later
### Ads always return `failed`
Narrow down by `reason`; each has a different cause.
| reason | Cause | Fixable in code? |
|---|---|---|
| `invalid_request` | Missing `placementId`, unregistered placement, placement type mismatch, or missing `requestId` for rewarded | Missing values: yes. Unregistered: **confirm the issuance with ONE store** |
| `no_fill` | No inventory | No. Only check that there is a path to continue without the reward |
| `already_showing` | Called again while an ad or purchase UI is showing | Prevent repeated taps, disable the button |
| `platform_not_supported` | This environment or app version is not supported | No. Hide the UI with `isSupported` |
| `timeout` | No response for the ad ending | Do not request the same placement again right away; guard against double grants with `requestId` |
| `show_failed` · `network_error` | Display or network failure | No. Ask for the reproduction conditions and pass them on |
### Rewards are not granted (the most common question)
**First check whether rewards are granted from the client-side `status: "rewarded"`.** That causes
most cases. The grant is confirmed through SSV → verification → the game company's server postback,
and the game has to ask its own server whether the reward was granted. If the code is right, hand
over postback endpoint registration and the `requestId` mapping (can the user be identified?) as
needs confirmation.
### Purchases fail
Narrow down by `reason` (the purchase reasons table in `sdk-context.md`).
- `already_owned` → buying an unconsumed product again. **Consumption is the game company's server's
job.** Confirm whether the server consumed it
- `need_login` / `need_update` → the device state. Only check that there is UI telling the user what to do
- `user_cancelled` → not an error. If an error popup appears, that is what to fix
- `already_processing` → called again while an ad or purchase UI is showing. Disable the button
- `invalid_request` → check for a missing `productId` or a `developerPayload` over 200 bytes
### The back key exits the whole game
Look at the `onBackPressed` handler. With no handler, the default (exit popup) is expected. If a
handler is registered and it still happens: is it **async** (no answer within 200 ms counts as not
consumed)? Does it return exactly `true` (truthy is not enough)? Does it throw?
### The game freezes after an ad or the background
Look at the `pause` / `resume` listeners: is the handling idempotent (they can fire more than once),
and does `resume` actually restart the game loop?
### Saves are lost
Check every path.
1. No `exit` listener → there is no chance to save on exit
2. The `exit` save is async or takes over 0.5 seconds → it gets cut off. Only synchronous work
3. The game calls `quit()` itself → `exit` does not fire on that path. Does it save before the call?
4. Saves depend on cookies or localStorage → storage partitioning in a cross-origin iframe can show a
different store. Server-side saves keyed by `playerId` are recommended
## Ask about what the code cannot tell
- Where it happened: a regular browser, or the ONE store app (web game test). The symptoms mean
different things
- If the test menu does not appear in the app: whether the test URL and test ID are registered in ONEconsole
- Whether the placementId and products (productId) are issued and registered with ONE store
- Whether the served game URL exactly matches the test URL registered in ONEconsole
- Whether the postback and PNS endpoints are registered and the server is receiving them
references/embed-requirements.md
# iframe embedding requirements: what the game company must do
The ONE store hosting page loads the game page in a **cross-origin iframe**. Without the steps
below, **the game does not load at all, or it loads but saving and navigation break.**
## 1. Allow framing (the most common failure)
What counts is the game URL's **final response headers** (after your CDN and WAF).
| Game URL response headers | Result |
|---|---|
| `X-Frame-Options: DENY` or `SAMEORIGIN` | Blocked |
| CSP `frame-ancestors` without the ONE store domains | Blocked |
| CSP `frame-ancestors` including the ONE store domains (including a wildcard such as `*`) | Allowed |
| Neither header present | Allowed (open to every site) |
If both headers are present, `frame-ancestors` takes precedence. `X-Frame-Options` cannot allow
specific domains only; `ALLOW-FROM` is deprecated syntax.
**Option 1: allow only the ONE store domains (recommended).** Keeps your existing blocking policy
while allowing ONE store.
```
Content-Security-Policy: frame-ancestors https://*.onestore.co.kr https://*.onestore.net
```
**Allow both domains as a whole, not individual hosts.** `frame-ancestors` checks **every ancestor
screen** that wraps the game. Besides the game runtime screen, the **registration review tool** also
wraps the game, and the two are on different ONE store domains. Listing only the runtime domain gets
the game **blocked at registration review and rejected.**
**Option 2: remove the blocking headers (alternative).** Remove **both** `X-Frame-Options` and CSP
`frame-ancestors`; if you remove only one, the other keeps blocking. This also removes clickjacking
protection, so any site could embed the game page. Choose it only if your security policy allows it.
- **Values written as `<meta http-equiv>` are ignored by browsers.** Both settings work only as HTTP
response headers
- They are often added by default by frameworks, CDNs, or WAFs even if you never set them (Spring
Security defaults to `DENY`, helmet to `SAMEORIGIN`). If blocking continues after checking the
application settings, check the layers in front of it
- ONE store domains **may change with prior notice.** With option 1, check the guide for the current
values periodically
- To check: if the screen is blank, look for `Refused to frame` or `frame-ancestors` messages in the
browser console
## 2. URL rules
- **HTTPS is required.** The parent page is HTTPS, so an HTTP game is blocked as mixed content
- Serve the game from the URL registered as the test URL in ONEconsole. Watch for `www` and
country-specific domain differences
- **Avoid redirects to another origin.** The game may fail to load or to communicate with the SDK
## 3. Third-party storage limits (easy to miss)
Storage rules differ in a cross-origin iframe.
- Cookies are not sent without `SameSite=None; Secure`
- Browser storage partitioning can show **a different store than when the site is visited directly**
- **Reconsider designs that keep the session or saves in cookies.** Server-side saves keyed by
`playerId` are recommended
- Take particular care when porting a game that used to run as a top-level page
## 4. Forbidden or unavailable
| Do not | Result or alternative |
|---|---|
| Access `window.top` / `parent.document` | SecurityError |
| `window.top.location = ...` (top-level navigation) | Not possible. Navigate externally through `sdk.openDeeplink(url)` (domain allowlist check) |
| `alert` / `confirm` / `prompt` / `print` | Do not use. Replace with your own UI |
| Frame-busting code (`if (window.parent !== window) top.location = ...`) | **Must be removed.** With it, the game does not load |
| Calling fullscreen or screen-orientation lock APIs directly | May not work inside the iframe. Do not depend on them |
## 5. Screen and input
- Use `100dvh` for the viewport height (`100vh` gets cut off on mobile) and
`overscroll-behavior: none` to stop bouncing
- Handle notches with `viewport-fit=cover` plus `safeArea` from `initializeAsync`
- Audio autoplay works only after a user gesture
references/game-template.html.txt
<!doctype html>
<!-- ONE store H5 Game SDK integration template, based on the official sample.
Replace: __SDK_VERSION__ (SDK version, starts with v, default v1.1.0)
TODO: placementId, productId, requestId issuing
UI text is a placeholder; match the game's own language. -->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Game title</title>
<style>
* { box-sizing: border-box; }
/* 100dvh: 100vh gets cut off on mobile. overscroll-behavior stops bouncing */
html, body { height: 100dvh; overflow: hidden; }
body { margin: 0; background: #101418; color: #e8eaed;
font-family: -apple-system, "Malgun Gothic", sans-serif;
display: flex; flex-direction: column; overscroll-behavior: none; }
/* Loading screen: removed when startGameAsync succeeds */
#boot { position: fixed; inset: 0; background: #101418; z-index: 10;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }
#boot .track { width: 180px; height: 6px; border-radius: 3px; background: #2a3040; overflow: hidden; }
#boot .fill { height: 100%; width: 0%; background: #ee3d3d; }
#boot p { margin: 0; font-size: 13px; color: #8b93a1; }
#boot.done { display: none; }
main { flex: 1; display: flex; flex-direction: column; align-items: center;
justify-content: center; gap: 20px; padding: 20px; }
.actions { display: flex; gap: 10px; }
.actions button { padding: 13px 18px; border: 0; border-radius: 10px;
font-size: 14px; font-weight: 600; color: #fff; cursor: pointer; }
.actions button:disabled { opacity: 0.4; }
#btnBuy { background: #ee3d3d; }
#btnAd { background: #3d6bee; }
#msg { min-height: 3em; font-size: 12px; color: #9aa4b2; text-align: center;
padding: 0 16px 16px; word-break: break-all; }
</style>
</head>
<body>
<div id="boot">
<p>Loading…</p>
<div class="track"><div class="fill" id="bootFill"></div></div>
</div>
<main>
<!-- Game screen goes here -->
<div class="actions">
<button id="btnBuy">Buy item</button>
<button id="btnAd">Watch an ad for a reward</button>
</div>
</main>
<div id="msg"></div>
<!-- Load the SDK at the top level of the game document, before the game scripts
(initialization fails inside a child iframe). A game script loaded with a plain
<script src> runs before this module; give it type="module" or defer. -->
<script type="module">
import { createSDK } from "https://h5sdk.onestore.net/lib/__SDK_VERSION__/onestore-h5-sdk.min.js";
function boot() {
const sdk = createSDK();
const $ = (id) => document.getElementById(id);
let ctx = {}; // the initializeAsync result (playerId, etc.)
function say(m) { $("msg").textContent = m; }
// Keep the game playable without the SDK: turn off only what needs the app
function runWithoutSdk(why) {
$("boot").classList.add("done");
$("btnBuy").disabled = true;
$("btnAd").disabled = true;
say("Running without ONE store features (" + why + ")");
}
function saveProgress() {
// TODO: save progress. Synchronous work that finishes within 0.5 seconds only.
// Key it by ctx.playerId (the pseudonymous per-game user ID).
}
/* ── Lifecycle: register before initializeAsync() ──────────── */
sdk.on("pause", (e) => {
// reason: "background" | "ad" | "payment". Handle it idempotently
// TODO: stop the game loop and sound
});
sdk.on("resume", (e) => {
// e.ringerSilent is present only if silent mode changed
// TODO: restart the game loop, apply e.ringerSilent
});
sdk.on("exit", () => {
// Only save. No need to call quit() again. Also fires on restart().
saveProgress();
});
sdk.onBackPressed(() => {
// Must be synchronous: the app waits 200 ms for the answer
// TODO: if an in-game popup is open, close it and return true (consumed)
return false; // not consumed → the app's exit confirmation popup
});
/* ── Boot: init → loading progress → start ─────────────── */
sdk.initializeAsync()
.then((info) => {
if (info.err) {
// Unsupported environment (for example a regular browser): it resolves
// with err, it does not reject. Keep the game running without the SDK so
// development keeps working, and turn off features that need the app.
runWithoutSdk(info.err);
return;
}
ctx = info;
// TODO: handle notches with info.safeArea, apply info.ringerSilent
startLoading();
})
.catch((e) => {
console.error("initializeAsync failed", e.reason, e.message);
runWithoutSdk(e.reason);
});
function startLoading() {
// TODO: report real asset loading progress as 0–100 (the timer below is a demo)
let progress = 0;
const timer = setInterval(() => {
sdk.setLoadingProgress(progress);
if (progress >= 100) {
clearInterval(timer);
sdk.startGameAsync()
.then(() => {
$("boot").classList.add("done");
// Feature checks are valid only after init
$("btnBuy").disabled = !sdk.iap.isSupported();
$("btnAd").disabled = !sdk.ads.isSupported("rewarded");
// Preloading early makes the ad show immediately when the button is
// pressed (call loadInterstitial too if you use interstitial ads)
if (!$("btnAd").disabled)
sdk.ads.loadRewarded({ placementId: "" /* TODO: placement ID */ });
})
.catch((e) => {
console.error("startGameAsync failed", e.reason, e.message);
runWithoutSdk(e.reason);
});
} else {
progress += 5;
}
}, 100);
}
/* ── In-app purchase ─────────────────────────────────────── */
$("btnBuy").addEventListener("click", () => {
$("btnBuy").disabled = true;
say("Processing purchase…");
sdk.iap.purchase({
productId: "" /* TODO: in-app product ID */,
// productName: "", // optional; if omitted, the registered product name is shown
// developerPayload holds what is needed to grant the item. Optional but very
// important: the purchase result and the PNS notification are matched to the
// user and purchase by this value. At most 200 bytes in UTF-8.
developerPayload: "" /* TODO: e.g. your user ID and an order key (ctx.playerId can be absent) */,
})
.then((r) => {
// A success response only signals that payment completed. The game
// company's server grants the item after verifying r.purchaseToken. The
// payment notification (PNS) also goes to the server, and consumption is the
// server's job too (buying an unconsumed product again gives already_owned).
say("Purchase complete: " + r.productId);
})
.catch((e) => {
// Branch on reason only. Do not show an error for user_cancelled
if (e.reason !== "user_cancelled") say("Purchase failed: " + e.reason);
})
.finally(() => { $("btnBuy").disabled = false; });
});
/* ── Rewarded ad ───────────────────────────────────────── */
$("btnAd").addEventListener("click", async () => {
// requestId is required. Issuing it on the server and mapping it to the
// user is recommended. The SDK does not generate it.
const requestId = "req-" + Date.now(); /* TODO: replace with a server-issued ID */
$("btnAd").disabled = true;
const r = await sdk.ads.showRewardedAsync({
placementId: "" /* TODO: placement ID */,
requestId,
});
// Failures do not throw; they come back in status
if (r.status === "rewarded") {
// Only a signal for the reward effect. The grant is confirmed through SSV →
// verification → the game company's server postback. Do not grant here.
say("Reward requested (waiting for the server postback)");
} else if (r.status === "dismissed") {
say("No reward: the ad was not watched to the end.");
} else {
// failed, including no_fill (no inventory), is not rewarded. Do not block progress
say("The ad could not be shown. (" + (r.reason || "failed") + ")");
}
$("btnAd").disabled = false;
});
/* ── Interstitial ad (optional) ────────────────────────────── */
// Call at a natural break, such as a stage clear. No reward and no requestId.
async function showInterstitial() {
if (!sdk.ads.isSupported("interstitial")) return;
const r = await sdk.ads.showInterstitialAsync({
placementId: "" /* TODO: interstitial placement ID */,
});
// completed or failed: continue the game either way
if (r.status === "failed") console.warn("interstitial failed", r.reason);
}
}
// Run boot even if the document already finished loading
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot);
} else {
boot();
}
</script>
</body>
</html>
references/sdk-context.md
# ONE store H5 Game SDK: API facts
This file is everything the skill knows about the SDK. **Do not invent classes, methods, fields, or
values that are not here.** The sources are the public type declarations of SDK v1.1.0 and the
official integration guide (v1.1.0, revised 2026-09-18). Public docs:
<https://onestore-dev.gitbook.io/dev/tools/web-sdk>
## What it is
A web SDK that connects an HTML5 game to the ONE store app (Android WebView). The ONE store hosting
page loads the game page in an iframe, and the SDK handles communication with the app. **The game
never deals with postMessage or the app bridge directly.**
- **The only supported environment is the WebView inside the Android ONE store app.** Regular
browsers and iOS are not supported. Detect the environment with `err` in the `initializeAsync()`
result and with each feature's `isSupported()`
- Feature areas: **lifecycle** (init, loading, start, pause, exit), **ads** (rewarded, interstitial),
**in-app purchase**
## Loading and entry point
```html
<script type="module">
import { createSDK } from "https://h5sdk.onestore.net/lib/{{version}}/onestore-h5-sdk.min.js";
const sdk = createSDK();
</script>
```
- There are **two distributed bundles**: `onestore-h5-sdk.js` and `onestore-h5-sdk.min.js`
(recommended). Put the version in `{{version}}`. It starts with `v`; if the user was not given
one, use **the latest, `v1.1.0`**
- **The SDK must load before the game scripts.** It connects to the hosting page as soon as it
loads; if it loads late, it can miss events that arrive right after initialization
- **Put the script that calls the SDK at the top level of the game document.** Initialization fails
inside an iframe the game page creates itself
- `createSDK()` returns the same instance no matter how many times it is called. It is the same
object as `window.ONEstoreH5`
- ES modules (`type="module"`) are blocked by CORS on the `file://` origin. Test locally over HTTP
## Environment and state
```ts
sdk.getSDKVersion(): string
sdk.getState(): "IDLE" | "INITIALIZING" | "READY" | "LOADING" | "PLAYING" | "PAUSED" | "TERMINATED"
```
- Detect the environment with `err` in the `initializeAsync()` result (unsupported environment) and
each feature's `isSupported()`. **Feature support is checked per feature:** `sdk.ads.isSupported()`,
`sdk.iap.isSupported()`
## Required call order (nothing works if it is broken)
```
initializeAsync() → setLoadingProgress(0–100) → startGameAsync() → (play) → quit() / restart()
```
```ts
sdk.initializeAsync(): Promise<InitResult>
sdk.setLoadingProgress(percentage: number): void
sdk.startGameAsync(): Promise<InitResult>
sdk.quit(): void // the game exits on its own. No exit event; save before calling
sdk.restart(): void // reload. An exit event gives a chance to save, then the app reloads the page
```
- `initializeAsync` is idempotent: calling it again returns the same Promise
- **In an unsupported environment it resolves with `err` instead of rejecting**, so the game's boot
flow is not interrupted. Branch with `if (info.err)`
- Calling `startGameAsync` without init rejects with reason `sdk_not_initialized`. On failure the
state returns to IDLE, so it can be retried
- After `restart()`, start again from `createSDK()` → `initializeAsync()`
### InitResult (returned by initializeAsync and startGameAsync)
| Field | Meaning |
|---|---|
| `playerId?` | **Pseudonymous per-game user ID.** The key the game server stores progress under |
| `locale?` | Device UI language (BCP-47, e.g. `ko-KR`) |
| `ringerSilent?` | Whether the device is in silent mode. Later changes arrive with the `resume` event |
| `safeArea?` | `{top, bottom, left, right}` (px): insets to keep clear of the system bars |
| `err?` | **Present only in unsupported environments** (e.g. `"Platform Not Supported"`). When set, the fields above are not provided |
## Lifecycle events
```ts
sdk.on(event, fn) / sdk.once(event, fn) / sdk.off(event, fn?)
```
**Register listeners before calling `initializeAsync()`.** Events can arrive in a row right after
initialization.
| Event | Payload | Rule |
|---|---|---|
| `pause` | `{ reason: "background" \| "ad" \| "payment" }` | Fires automatically when the app goes to the background, an ad shows (`"ad"`), or a purchase is in progress (`"payment"`). The game only needs to handle it idempotently |
| `resume` | `{ ringerSilent?: boolean }` | The field is present only if silent mode changed between pause and resume |
| `exit` | none | **Only save. No need to call `quit()` again.** The SDK and the app finish the exit |
`exit` rules (all important):
- Saving must finish **synchronously within 0.5 seconds**. The app closes the game screen or reloads
the page within that time. Async saves (fetch, IndexedDB, and so on) are not guaranteed to finish
- It also fires on the `restart()` path. Unsaved progress is lost
- It does not fire when the game calls `quit()` itself. In that case, save before calling it
- A game with no listener exits immediately when an exit is requested
- `onClose(fn)` is an alias of `on("exit", fn)`. **Use only `on("exit")` in code and guidance**, so
the game does not end up registering the same handler twice
## Back key
```ts
sdk.onBackPressed(handler: (() => boolean) | null): void
```
- Return `true` (consumed) if the game closed its own UI (a popup, etc.), otherwise `false`, which
shows the app's exit confirmation popup
- **Must be a synchronous function.** The app waits 200 ms for the answer. No async
- No handler, or a handler that throws, counts as not consumed
- Only an exact `true` counts as consumed
## Ads (`sdk.ads`)
```ts
sdk.ads.isSupported(type?: "rewarded" | "interstitial"): boolean // valid only after init
sdk.ads.loadRewarded({ placementId }): void // preload: accepted only, no completion callback
sdk.ads.loadInterstitial({ placementId }): void // (recommended, see below)
sdk.ads.isReadyAsync(type, placementId): Promise<boolean> // a hint for showing the ad button
sdk.ads.showRewardedAsync({ placementId, requestId, timeoutMs? }): Promise<AdResult>
sdk.ads.showInterstitialAsync({ placementId, timeoutMs? }): Promise<AdResult>
```
`isSupported()` with no argument is true only when both rewarded and interstitial are supported.
### AdResult
| Field | Meaning |
|---|---|
| `status` | `"rewarded"` \| `"completed"` \| `"dismissed"` \| `"failed"` |
| `requestId?` | The value passed in the request, returned as is. **Returned on failure and timeout too.** Not present for interstitial ads |
| `reason?` | Failure reason (`no_fill`, `already_showing`, …). Branch on this |
Key rules (the most misunderstood area):
- **`placementId` must be issued in advance.** ONE store issues it per game, separately for rewarded
and interstitial. A request with an unissued placement fails with no fallback
- **Preloading (`loadRewarded` / `loadInterstitial`) early, when the game starts, is recommended**, so
the ad shows immediately when the button is pressed. Without it the ad still loads when requested,
but the first one takes longer. Calling it repeatedly for the same placement does not load twice
- Rewarded results are `rewarded` | `dismissed` | `failed`; interstitial results are `completed` | `failed`
- **Show the reward only on `rewarded`.** `dismissed` (left early) and `failed` (including `no_fill`)
are not rewarded. The policy is no reward when an ad could not be shown for lack of inventory
- **Do not grant rewards from the client-side `rewarded` result.** The grant is confirmed through
SSV (server-side verification) → verification → the game company's server postback. `rewarded` is
only a signal for the reward effect
- **`requestId` is required for rewarded ads.** Without it the request fails with `invalid_request`
without calling the app. The SDK does not generate it. The postback has to identify the user, so
**issuing it on the game company's server and mapping it to the user is recommended** (not required)
- Failures **do not throw**. They come back as `status: "failed"` with a `reason`
- A missing `placementId`, an unregistered placement, or a placement type mismatch (a rewarded ID
used for an interstitial, or the reverse) fails with `invalid_request`
- Repeated taps are blocked by the SDK (`reason: "already_showing"`). Only one full-screen UI (ad or
purchase) can be shown at a time
- `pause` / `resume` fire automatically around an ad. The game does not need to do anything extra
- `timeoutMs` is a backstop **until the ad finishes**. Default 600000 (10 minutes), allowed
1000–1800000 (30 minutes); out-of-range values fall back to the default. Usually not needed
- An ad can still show late after a `timeout` failure. Avoid requesting the same placement again
right away, and guard against double grants with the returned `requestId`
- Requesting an ad while unsupported returns `{ status: "failed", reason: "platform_not_supported" }`
without calling the app
- `isReadyAsync` returns `false` for an unregistered placement, a placement type mismatch, a lookup
failure, or a missing `placementId` (it does not throw)
## In-app purchase (`sdk.iap`)
```ts
sdk.iap.isSupported(feature?: "purchase" | "getProductDetails"): boolean // valid only after init
sdk.iap.purchase({ productId, productName?, developerPayload? }): Promise<PurchaseResult>
sdk.iap.getProductDetailsAsync(productIds: string[]): Promise<ProductDetails[]>
```
**`isSupported()` with no argument means `"purchase"`** (whether purchase is supported). This differs
from ads, where no argument means both. Always check product lookup support with
`isSupported("getProductDetails")`: some app versions support purchase but not lookup.
Preparation: register products in **ONEconsole: 수익화 > 인앱 상품 (Monetization > In-App
Products)**. Web products support **managed products only** (no subscriptions).
- `productId` is required: the custom product ID registered in the Developer Center. `productName`
is optional (if omitted, the registered product name is shown)
- `developerPayload` **holds the information needed to grant the item.** It is optional but **very
important**: when the game receives the purchase result or the server receives the PNS (payment
notification), this value identifies which user and which purchase it is. Put in what is needed
to grant the item, such as a user identifier, and map it on the server. **Up to 200 bytes in
UTF-8.** Longer values reject with `invalid_request` without calling the app
- It waits until the purchase UI ends (success, failure, or cancel). **There is no SDK timeout**
- **Purchase requests trigger `pause` / `resume`, and the `pause` reason is `"payment"`.** The
purchase call site does not need to pause the game; handle it consistently in the `pause` listener
- Failures reject with `SDKError`. Branch on `reason` only
### PurchaseResult
| Field | Meaning |
|---|---|
| `orderId` | Order ID (receipt information) |
| `purchaseTimeMillis` | Purchase time (epoch millis) |
| `purchaseId` | Purchase ID |
| `purchaseToken` | Purchase token. **Use it for server-side verification** |
| `productId` | The requested in-app product ID |
| `developerPayload?` | Only if it was passed in the request |
Granting rules (all important):
- A successful `purchase()` response **only signals that payment completed; it is not a reason to
grant the item.** The game company's server grants the item after verifying the payment with
`purchaseToken`
- Payment completion is **notified to the game company's server via PNS**. PNS arrives even if the
client never gets the response (network loss, app killed), so **build granting around the server**
- **Consumption is also the game company's server's job.** The app does not consume purchases
automatically, so buying an unconsumed product again fails with `already_owned`. See the ONE store
server API documentation
### Purchase reasons
| reason | Meaning |
|---|---|
| `invalid_request` | Missing required parameter, bad format, or `developerPayload` over 200 bytes |
| `user_cancelled` | The user cancelled the purchase |
| `service_unavailable` | Network or processing error on the device or server |
| `already_owned` | Buying an unconsumed product again |
| `payment_failed` | Payment failed |
| `need_login` | Not signed in to the ONE store app |
| `security_error` | Abnormal purchase request, or a required permission was denied |
| `already_processing` | A purchase or ad UI is already showing (only one full-screen UI at a time) |
| `need_update` | The ONE store service app is missing or outdated. Tell the user to update the app |
| `internal_error` | Internal error |
| `platform_not_supported` | This environment or app version does not provide the feature. Check purchase with `sdk.iap.isSupported()` and lookup with `sdk.iap.isSupported("getProductDetails")` beforehand |
### Product lookup (`getProductDetailsAsync`)
For showing prices in the game's shop. It does not show a purchase screen, so it can be called while
an ad or purchase is showing (it is not subject to the full-screen limit or pause / resume).
- **Works on ONE store service app v8.5.0 or later** (release notes, v1.1.0 update of 2026-09-17).
On older apps `isSupported("getProductDetails")` is false; continue without showing prices
- **Up to 20 `productIds` per call.** A non-array, an empty array, more than 20, or an empty string
fails with `invalid_request` without calling the app. For 21 or more, split into calls of 20
- The response is `ProductDetails[]`: `productId` (the requested value), `type` (`"inapp"`), `title`,
`price` (display string), `priceAmountMicros` (price × 1,000,000, for math and sorting),
`priceCurrencyCode` (e.g. `"KRW"`)
- **An empty array is not an error.** Products that are not on sale or not registered are left out
(partial results), and the order does not follow the request. Match by `productId`
- `price` is a string. Do not use it for arithmetic; use `priceAmountMicros`
- Unsupported → `platform_not_supported` without calling the app. No response in 10 seconds →
`timeout`. Other failure reasons follow the purchase table
## External links
```ts
sdk.openDeeplink(url: string): Promise<void>
```
The app validates the allowed domains. Both an unlisted domain and a format error reject with
`invalid_params` (they are not distinguished). Navigating directly, for example with
`window.top.location`, does not work inside the iframe; external navigation must go through this API.
## Error model
Errors the game sees are `SDKError`. **Branch on the `reason` string only.** Numeric error codes are
not passed to the game. `message` is supplementary text for logs; do not branch on it.
| Group | reason |
|---|---|
| Common | `invalid_params` · `unsupported_action` · `platform_not_supported` · `user_cancelled` · `rate_limited` · `timeout` · `internal_error` |
| Ads | `invalid_request` · `unsupported_screen` · `not_initialized` · `no_fill` · `show_failed` · `already_showing` · `network_error` |
| Purchase | The "Purchase reasons" table above |
| SDK checks | `sdk_not_initialized` (call order violation) |
Treat an unknown reason like `internal_error`.
## Debugging
```ts
sdk.setDebugLogger(fn: (msg: string) => void): void
```
Exposes the SDK's internal decisions as strings. **The first tool for integration debugging.**
Remove it from release builds.
## Device testing
How to run the game inside the ONE store app. Follow the order, or the menu does not appear.
1. **Register the test URL.** In ONEconsole, go to Apps > 상품관리 > Web 상품관리 > 기본정보
(Apps > Product Mgmt. > Web App Mgmt. > Main Info), enter the game address in the test URL
field, and save
2. **Register a test ID.** Register the account you will test with as a test ID. **Without it, the
menu in step 3 does not appear in the app.** Choose Sandbox as the payment environment to avoid
real charges
3. **Open web game testing.** In the ONE store app signed in with that account, go to
마이 > 이용 안내 > 웹게임 테스트 > 내 테스트 게임 (My > User Guide > Web Game Test > My Test Games)
and select the game
4. **Test purchases.** Follow the ONE store purchase test procedure
If the menu does not appear, check step 2 (test ID registration) first.
SKILL.md
---
name: onestore-web-sdk
description: 'Integrate the ONE store H5 Game SDK (Web SDK) into an HTML5 web game, check the integration status, and review the code before release. Use when the conversation mentions ONE store, OneStore, 원스토어, web game, 웹게임, HTML5 game, H5, WebView game, in-app purchase, 인앱결제, ads, 광고, rewarded or interstitial ads, createSDK, ONEstoreH5, initializeAsync, startGameAsync, purchase, getProductDetailsAsync, showRewardedAsync, or an iframe-hosted game, or when the user sends a command word such as doctor, integrate, iap, ads, embed, fix, or check to this skill. Covers loading the SDK, the boot flow (init, loading, start), lifecycle handling, ad and purchase code, iframe embedding requirements, error (reason) diagnosis, a blank screen in the app, and pre-release checks. When asked to write ad, purchase, or lifecycle code for a ONE store web game, use this skill instead of writing it from scratch, because it contains reference code based on the official sample.'
---
# ONE store H5 Game SDK integration
Helps HTML5 game developers add the ONE store H5 Game SDK, diagnose problems, and check the
game before release. The ONE store app (Android WebView) loads the game page in an iframe, and
the SDK handles all communication with the app.
> [!IMPORTANT]
> **Answer in the user's language.** This skill is written in English, but most users write in
> Korean. Answer in the language the user writes in, and translate everything you show: table
> headers, verdict words, fill-in forms, and the suggested messages at the end. Keep only the
> structure. Keep code identifiers, SDK constants, and `reason` strings unchanged. Menu paths in
> this skill are given as `Korean label (English label)`; use the one that matches the user's
> language. Before sending, check that the first sentence, the table headers, and the closing
> suggestion are in the user's language.
## What this skill does
If the user asks what the skill can do, or invokes it with no request, show the table below and
run the status check right away. Users can ask in plain language; they do not need to know the
procedure names.
| Command | Plain-language example | What to do |
|---|---|---|
| `doctor` | "How far along is the ONE store integration?" / "지금 어디까지 됐어?" | **Status check.** Report what is present and what is missing in a table. Do not edit files |
| `integrate` | "Add the ONE store SDK so I can publish this game" / "원스토어 SDK 붙여줘" | Load the SDK, wire the boot flow (init → loading → start), place the lifecycle listeners |
| `ads` | "Add a rewarded ad" / "리워드 광고 붙여줘" | Place ad code, including `requestId` and the reward policy |
| `iap` | "Add in-app purchase" / "결제 붙여줘" | Place purchase and product lookup code, including the hook for server-side `purchaseToken` verification |
| `embed` | "It only fails inside the app" / "앱에서만 안 떠" | Check the iframe embedding requirements (`references/embed-requirements.md`) |
| `fix <reason or symptom>` | "The screen is blank" / "reason is no_fill" | **Diagnose.** Cause, reproduction, fix, and what to verify (`references/diagnose.md`) |
| `check` | "Run the pre-release check" / "릴리즈 전에 점검해줘" | The full pre-release checklist. Do not edit files |
### Command words
A user can call a procedure directly with a command word. In Claude Code the form is
`/onestore-web-sdk <command>` (for example `/onestore-web-sdk doctor`), and the word arrives as the
skill argument (`ARGUMENTS: doctor`). Other tools invoke skills differently, and some users just
type `onestore doctor` or `/원스토어 진단` in chat. Treat all of these the same way.
- **A command word alone means: run that procedure now.** Do not ask what they want. Ask only if
the target file is unclear (rule 3)
- The skill invoked with no command word → run the status check and show the table above
- When you suggest the next step, give the plain-language request first, because it works in every
tool, and add the Claude Code form after it
State the boundary as well: the game company's server work (receiving postbacks, verifying
`purchaseToken`, consuming purchases) and ONE store registration (placement IDs, products, test
URL) are the user's job, and this skill does not cover the ONE store hosting page or app.
### Values you need from the user
**There are only four: three come from the ONE store side, and one is the game company's server design.** Many users start without knowing
what is needed, so **tell them up front and say where to get each one.**
| Value | Where it comes from | If missing |
|---|---|---|
| **SDK version** | The user's ONE store contact, or the [release notes](https://onestore-dev.gitbook.io/dev/tools/web-sdk/releasenote). Starts with `v`. **If they were not told otherwise, recommend the latest, `v1.1.0`** | You can proceed with `v1.1.0` |
| **Ad placement ID (`placementId`)** | Requested in ONEconsole: Apps > (the game) > 수익화 > 인앱 광고 (Monetization > In-App Ads), one per ad type (rewarded, interstitial). See [ONEconsole: 인앱 광고 (In-App Ads)](https://onestore-dev.gitbook.io/dev/docs/apps/product/monetization/iaa) | Every ad request fails, with no fallback |
| **In-app product ID (`productId`)** | ONEconsole: Apps > (the game) > 수익화 > 인앱 상품 (Monetization > In-App Products). Managed products only | Purchases fail with `invalid_request` |
| **How `requestId` is issued** | The game company's server design. Issuing it on the server and mapping it to the user is recommended | You cannot tell whose reward a postback belongs to |
#### When you ask for values, give a form to fill in
**Saying only "X is missing" leaves the user unsure what to fetch and from where.** Send the form
below as is, translated into the user's language, so they can copy it, fill it in, and paste it back.
```text
SDK version: (leave blank to use the latest, v1.1.0)
Fill this in only if ONE store gave you a specific version. It starts with v.
It goes into the {{version}} part of the CDN URL.
Ad placement ID: (write it here, e.g. rewarded rew_xxxx, interstitial int_xxxx)
Request it in ONEconsole: Apps > (the game) > 수익화 > 인앱 광고
(Monetization > In-App Ads). Rewarded and interstitial
IDs are issued separately, so say which one this is.
In-app product ID: (write it here, e.g. gem_100, gem_1000)
The product ID registered in ONEconsole under 수익화 > 인앱 상품 (Monetization > In-App Products).
If there are several, list them all separated by commas. (Managed products only.)
requestId issuing: (server-issued / generated on the client)
The tracking key for rewarded ad rewards. The postback must tell you which user
the reward belongs to, so issuing it on your server and mapping it to the user
is recommended.
```
- **Leave the value slots empty.** If you pre-fill example values, some users leave them as is
- **Do not use real values as examples.** Do not put IDs you found in the project into the form.
Mention values you already know in a sentence, and leave the form empty
- If something is not registered or issued yet, say that **the ONE store side has to be done first**
- When you receive values, **say which file you put them in**
**Integration can start without the values.** Load the SDK, wire the boot flow, and place the
listeners, leave the value slots as TODOs, then give the form and stop. **Do not block progress
while waiting for values.**
### When the next step is yours, give the words to say
**When the remaining work is yours, the only thing the user has to do is ask for it.** If you
describe the work instead, the user thinks they have to do it and gets stuck. **Give the words,
not the task:** a request they can copy and send, in a code block so it stands out.
```text
Send this next:
Add a ONE store rewarded ad to the game
I will wire it with the reward policy (show the reward only on rewarded, grant it through
the server postback). In Claude Code you can also send /onestore-web-sdk ads.
After that:
Run the ONE store pre-release check
```
- **Pair each request with its result**, so the user can see what each one does and choose
- **If more than one step remains, list them all in order.** Giving only the current one leaves
the user stuck again at every step
- If the user's work (ONE store registration, server code) and yours are mixed, **list them separately**
## Core rules
1. **Do not invent SDK APIs.** Use only the methods, fields, events, and `reason` values in
`references/sdk-context.md`. If something is not there, say it needs confirmation. Do not map
names from other platforms' game SDKs (such as Facebook Instant Games) even if they look alike.
2. **Take the integration code from `references/game-template.html.txt`.** It is reference code,
based on the official sample, for the boot flow, ads, purchase, and lifecycle. When adding it to an existing game,
move the relevant parts but **keep the call order and branch structure**, especially the `err`
branch, the reward branch (`rewarded` only), and the `reason` branches. Adjust after placing it
if it does not fit.
3. **Decide the target from evidence.** If there is no evidence of where the game's entry HTML and
boot code are, or there are several candidates, ask instead of guessing.
4. **Answer definitely or ask. Do not hedge.** Do not dodge with "probably" or "usually". If you
lack information needed for a verdict, ask for that specific information.
5. **Cite evidence as `path:line`.** Use the path relative to the project root plus the line number
(for example `index.html:47`). Tools render links differently, so this form reads everywhere and
is clickable in most tools. Do not use absolute paths or `file://` links. Fix any verdict that
has no evidence location. Point to public guide URLs for further reading; the reference files
are for you, not for the user.
6. **End with the next step.** Do not end with what you did. If the next step is the user's, make
it concrete enough to do on the spot (menu path, complete command). If it is yours, give a
request they can send. If you stopped for missing values, say what to provide to continue. If
nothing is left, say so. **Hearing "what do I do now?" means this rule was not followed.**
7. **Do not overwrite existing files.** Show what is there and ask.
8. **The scope is the game client code.** For the game company's server (postback and PNS
reception, `purchaseToken` verification, consumption), create the hook only and say the
implementation is the user's. Do not set up a server framework.
## First: status check (doctor)
For open-ended requests ("add it", "how far along is it?", "what now?"), when the skill is invoked
with no request, or when you do not know the project state yet, **run the status check first**. Do
this even in the middle of an integration: read the files to decide, do not answer from memory.
- Follow **list A** in `references/checks.md`
- **Read only. Do not edit any file**
- Use exactly four verdicts: **present / missing / not applicable / needs confirmation**. Cite
evidence as `path:line` (rule 5)
- **Show the full table.** The point is to show what is already done too (the pre-release check is
the opposite: only what needs fixing)
- Under the table, give the next step (rule 6). If values are missing, give the form (only the
missing values); if the next step is yours, give the request to send
- Unless the user already told you to proceed, stop here and let them choose
## Integration steps
Do only what the status check found missing.
### 1. Load the SDK
Put it at the **top level of the game's entry HTML document**, **before the game scripts**.
```html
<script type="module">
import { createSDK } from "https://h5sdk.onestore.net/lib/v1.1.0/onestore-h5-sdk.min.js";
</script>
```
- The version starts with `v`. Use the one the user was given, otherwise **the latest, `v1.1.0`**.
Do not stop progress over the version
- Initialization fails inside an iframe the game creates itself. It must be the game document's
top level
- If the game uses a bundler (webpack etc.), put the import at the very top of the game entry and
check that it runs in the same order as loading it directly from HTML
- **Tag order alone does not guarantee execution order.** `type="module"` runs after the document
is parsed, so a game script loaded with a plain `<script src>` runs **before** the SDK even if its
tag comes later. Add `type="module"` or `defer` to the game script so it runs after the SDK, and
start the game only after `startGameAsync()` succeeds
### 2. Boot flow and lifecycle
Place the script part of `references/game-template.html.txt` to fit the game's boot code. The
required call order and rules are defined in `references/sdk-context.md`. After placing it, always tell
the user:
- **Register the listeners (`pause` / `resume` / `exit`) before `initializeAsync()`.** Events can
arrive right after initialization
- **In `exit`, only save, synchronously, within 0.5 seconds.** No need to call `quit()` again. When
the game calls `quit()` itself, `exit` does not fire, so save before calling it
- **`onBackPressed` must be a synchronous function.** The app waits 200 ms for the answer
- An unsupported environment resolves with `err` instead of rejecting. Do not remove the
`if (info.err)` branch. In that branch, keep the game playable without the SDK (turn off only
ad and purchase UI), so it still runs in a regular browser during development
- Connect the loading progress to real asset loading. The demo timer in the template is a placeholder
- **When the game keeps an object in a global, do not give it the same name as an element `id`.**
Browsers expose elements with an `id` as `window` properties of the same name. With
`<canvas id="game">`, `window.game` is the canvas until the game code overwrites it, and boot code
that runs first breaks with `window.game.start is not a function`. Use a global named after the
game (for example `window.StarCatcher`), and have the boot code check that the object is ready
before calling it
- **When the SDK code and the game code are in separate scripts, wait for the game object before
calling it.** The SDK script runs first, and the `initializeAsync()` result can arrive before the
game script has run (in a regular browser it resolves almost at once). Calling the game object
then throws, and the game never starts. Route every call from SDK code into the game through a
helper like this (the `err` branch, after `startGameAsync`, `pause` / `resume`, `ringerSilent`):
```js
// Game scripts load with type="module" or defer, so they have all run by DOMContentLoaded
function whenGame(fn) {
if (window.MyGame) fn(window.MyGame);
else document.addEventListener("DOMContentLoaded", () => fn(window.MyGame), { once: true });
}
// whenGame((game) => game.start());
```
This works when the game object is created at the top level of its script. If the game creates
it later (after loading assets, for example), have the game send its own "ready" signal instead
### 3. Ads
Check that `placementId` exists before placing the code. If it does not, place the code with TODOs
and ask for the value with the form. After placing it, always tell the user these three things,
because each one causes incidents:
- **Show the reward only when `status === "rewarded"`.** `dismissed` and `failed` (including
`no_fill`) are not rewarded
- **Do not grant rewards from the client-side `rewarded` result.** The grant is confirmed through
SSV → verification → the game company's server postback, and the game asks its own server whether
the reward was granted. Say separately that receiving the postback is the user's job
- **`requestId` is required for rewarded ads.** The SDK does not generate it. Issuing it on the
server and mapping it to the user is recommended
If the user has no placement ID yet, tell them what to do in ONEconsole, in this order (source:
[ONEconsole: 인앱 광고 (In-App Ads)](https://onestore-dev.gitbook.io/dev/docs/apps/product/monetization/iaa), English
<https://onestore-dev.gitbook.io/dev/eng/docs/apps/product/monetization/iaa>):
Both menus are in the game's sidebar after choosing the game under Apps.
1. Set 광고 적용 여부 (use ads) to 예 (Yes) under 상품관리 > Web 상품관리 > 기본정보
(Product Mgmt. > Web App Mgmt. > Main Info)
2. In 수익화 > 인앱 광고 (Monetization > In-App Ads), press
ID 발급 요청 (request ID) for each ad type. One ID per type; issuance takes about one business day
3. For rewarded ads, once the ID is issued, register the callback URL and API key on the same screen.
The server postback (SSV) that confirms rewards goes to that URL. Not needed for interstitial only
4. No test placement IDs are provided. If the user needs one, they ask devhelper@onestore.net
Place the preload calls (`loadRewarded` / `loadInterstitial`) **early, for example right after
`startGameAsync` succeeds**, so the ad shows immediately when the button is pressed. Without them it
still works, but the first ad takes longer.
Ad support has not yet been verified end to end on a device with this skill. Tell the user to test
ads on a device before release.
#### When the user provides a placement ID later
If the ad code was placed with an empty ID, receiving the ID means more than filling in one value.
Do all of the following:
1. **Put each ID in its own slot.** The rewarded ID goes where `loadRewarded` / `showRewardedAsync`
use it; the interstitial ID goes where `loadInterstitial` / `showInterstitialAsync` use it. If it
is unclear which one the user sent, ask. The two are issued separately, and a placement type
mismatch fails with `invalid_request`
2. **Turn back on what was turned off because the ID was empty**: a disabled ad button, a skipped
preload, an early `return`, a "not set" label. Filling in the constant alone often leaves the
button disabled
3. Keep the order: feature check (`sdk.ads.isSupported(...)`) after init → preload → enable the button
4. **Say which files and lines changed** (`path:line`)
5. Tell the user how to check it: ads show only inside the ONE store app (a regular browser always
returns `platform_not_supported`), with the test URL and test ID registered. A `no_fill` result
during testing means there is no ad inventory right now, not that the integration is broken
### 4. Purchase
Check that `productId` exists before placing the code. If it does not, place the code with TODOs and
ask for the value with the form. After placing it, always tell the user:
- **A success response is not a reason to grant the item.** The game company's server confirms the
purchase by verifying `purchaseToken`. The payment notification (PNS) also goes to the server, so
grant items based on the server
- **Consumption is the server's job.** Without it, buying the same product again fails with
`already_owned`
- **Put the information needed to grant the item in `developerPayload`.** It is optional but very
important: the purchase result and the PNS notification are matched to the user and purchase by
this value, so leaving it empty removes the way to match them. Suggest what to put in it (such as a
user identifier) based on the game's structure, but do not decide it by guessing
- Do not show an error popup for `user_cancelled`
- Purchase requests trigger `pause` (`reason: "payment"`) and `resume` events. Do not pause the game
separately at the purchase call site; handle it in the one `pause` listener
- Web products support **managed products only** (no subscriptions)
### 5. Embedding requirements
These are outside the code and easy to miss. Check them with `references/embed-requirements.md`, and
hand over server settings (frame-allow headers, HTTPS, redirects) as needs confirmation, with how to
check them (`curl -sI`). Tell the user that **any frame-busting code must be removed**.
## Error diagnosis
| What you received | Where to start |
|---|---|
| A `reason` string or console log | Look it up in the `reason` tables in `references/sdk-context.md`, and point to where the project handles that path |
| **Only a symptom, such as "it doesn't show up"** | **`references/diagnose.md`**, which says what to collect and how to narrow it down |
Do not start reading code with only a symptom. The cause is often outside the code (server headers,
ONE store registration, app version). Check what you can yourself (`curl -sI`, reproducing in the
console); if you cannot run it, give the command and steps and ask for the result. **Do not present
a guess as the answer.**
Answer with **cause, reproduction, fix (with code), and what to verify**. For reasons the code
cannot fix (`no_fill`, `platform_not_supported`, and so on), say so and give the right handling
(hide the UI, provide another path).
## Pre-release check
Follow **list B** in `references/checks.md`. Read only. For items that depend on the server or ONE
store registration, check only that the code has the hook, and say the rest is outside the project.
**List only what needs fixing.** Show passed items as a count. Order: blockers → non-blockers →
needs confirmation → passed count.
## Reference files
Read only what you need.
- `references/sdk-context.md`: the full public API, call order, events, `reason` tables, CDN URL,
and granting rules (the source of truth)
- `references/game-template.html.txt`: the reference integration code (replace `__SDK_VERSION__`,
fill in the TODOs)
- `references/embed-requirements.md`: iframe embedding requirements (frame allowance, URL, storage,
forbidden calls, screen)
- `references/checks.md`: list A (status check) and list B (pre-release check)
- `references/diagnose.md`: how to narrow down a problem from a symptom
Public docs: Web SDK Integration Guide <https://onestore-dev.gitbook.io/dev/tools/web-sdk> (English
<https://onestore-dev.gitbook.io/dev/eng/tools/web-sdk>)