@ait-co/devtools 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +867 -0
- package/README.md +108 -14
- package/dist/panel/index.js +125 -10
- package/dist/panel/index.js.map +1 -1
- package/dist/{tunnel-BbcgVy4L.js → tunnel-CY1velpk.js} +20 -7
- package/dist/tunnel-CY1velpk.js.map +1 -0
- package/dist/{tunnel-DeXfLGRl.cjs → tunnel-DgiECOnW.cjs} +20 -7
- package/dist/tunnel-DgiECOnW.cjs.map +1 -0
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +20 -6
- package/dist/unplugin/tunnel.cjs.map +1 -1
- package/dist/unplugin/tunnel.d.cts +13 -4
- package/dist/unplugin/tunnel.d.cts.map +1 -1
- package/dist/unplugin/tunnel.d.ts +13 -4
- package/dist/unplugin/tunnel.d.ts.map +1 -1
- package/dist/unplugin/tunnel.js +20 -7
- package/dist/unplugin/tunnel.js.map +1 -1
- package/package.json +8 -3
- package/dist/tunnel-BbcgVy4L.js.map +0 -1
- package/dist/tunnel-DeXfLGRl.cjs.map +0 -1
package/README.en.md
ADDED
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
# @ait-co/devtools
|
|
2
|
+
|
|
3
|
+
[한국어](./README.md) · **English**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@ait-co/devtools) [](./LICENSE)
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
A mock library for the `@apps-in-toss/web-framework` SDK. Imports of `@apps-in-toss/web-bridge` and `@apps-in-toss/web-analytics` are also mocked.
|
|
10
|
+
|
|
11
|
+
Lets you develop and test Apps in Toss mini-apps in a **regular browser** — without the Toss app. All SDK features are simulated so you can move fast.
|
|
12
|
+
|
|
13
|
+
- **60+ SDK API mocks** — auth, payments, IAP, location, camera, storage, and more
|
|
14
|
+
- **Device API mode system** — switch between mock / web / prompt modes for device APIs
|
|
15
|
+
- **Device simulation** — iPhone/Galaxy presets + orientation toggle to simulate a mobile viewport in your desktop browser
|
|
16
|
+
- **Floating DevTools Panel** — control SDK state in real time from the browser (10 tabs, mock state preset library included)
|
|
17
|
+
- **All bundlers supported** — [unplugin](https://github.com/unjs/unplugin)-based Vite, Webpack, Rspack, esbuild, and Rollup integration
|
|
18
|
+
|
|
19
|
+
Live demo: <https://devtools.aitc.dev/> (the `e2e/fixture/` from this repo deployed to GitHub Pages as a self-contained demo).
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -D @ait-co/devtools
|
|
25
|
+
# or
|
|
26
|
+
pnpm add -D @ait-co/devtools
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> **Supported SDK version**: `@apps-in-toss/web-framework >=2.5.0 <2.6.0` (peer, required).
|
|
30
|
+
>
|
|
31
|
+
> devtools is only verified against SDK versions within that range. Installing an out-of-range SDK version
|
|
32
|
+
> will cause the package manager to emit a peer warning at install time. Additionally, calling an API that
|
|
33
|
+
> devtools has not yet mocked will throw a runtime error — this is intentional to prevent the
|
|
34
|
+
> "works in devtools but fails with the real SDK" type of production incident. For missing APIs,
|
|
35
|
+
> please [file an issue](https://github.com/apps-in-toss-community/devtools/issues).
|
|
36
|
+
|
|
37
|
+
## Reference consumer
|
|
38
|
+
|
|
39
|
+
[`sdk-example`](https://github.com/apps-in-toss-community/sdk-example) is the reference consumer of devtools. It's a catalog app where every SDK API can be run interactively, and the web demo is live at <https://sdk-example.aitc.dev/>. When you add a new mock, confirming that it works on the sdk-example card is the first sanity check. That said, this repo's E2E suite runs against an **internal self-contained fixture (`e2e/fixture/`)** without cloning sdk-example — so a broken sdk-example won't affect devtools CI.
|
|
40
|
+
|
|
41
|
+
## Bundler setup
|
|
42
|
+
|
|
43
|
+
### Vite
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// vite.config.ts (development only)
|
|
47
|
+
import aitDevtools from '@ait-co/devtools/unplugin';
|
|
48
|
+
|
|
49
|
+
export default {
|
|
50
|
+
plugins: [aitDevtools.vite()],
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> This is a development-only setup. To exclude it from production builds, see the [Production builds](#production-builds) section below.
|
|
55
|
+
|
|
56
|
+
### Webpack / Rspack
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// webpack.config.js (ESM, recommended for development only)
|
|
60
|
+
import aitDevtools from '@ait-co/devtools/unplugin';
|
|
61
|
+
config.plugins.push(aitDevtools.webpack());
|
|
62
|
+
|
|
63
|
+
// webpack.config.js (CommonJS)
|
|
64
|
+
const aitDevtools = require('@ait-co/devtools/unplugin');
|
|
65
|
+
config.plugins.push(aitDevtools.webpack());
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Next.js (Turbopack)
|
|
69
|
+
|
|
70
|
+
Turbopack does not support a plugin system, so use `resolveAlias` instead.
|
|
71
|
+
|
|
72
|
+
- You also need to alias `@apps-in-toss/web-bridge` and `@apps-in-toss/web-analytics`.
|
|
73
|
+
- Turbopack is generally only used with `next dev`, so no extra production guard is needed.
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
// next.config.js (Next.js 15+)
|
|
77
|
+
module.exports = {
|
|
78
|
+
turbo: {
|
|
79
|
+
resolveAlias: {
|
|
80
|
+
'@apps-in-toss/web-framework': '@ait-co/devtools/mock',
|
|
81
|
+
'@apps-in-toss/web-bridge': '@ait-co/devtools/mock',
|
|
82
|
+
'@apps-in-toss/web-analytics': '@ait-co/devtools/mock',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For Next.js 14 and below, use `experimental.turbo`:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
// next.config.js (Next.js 14 and below)
|
|
92
|
+
module.exports = {
|
|
93
|
+
experimental: {
|
|
94
|
+
turbo: {
|
|
95
|
+
resolveAlias: {
|
|
96
|
+
'@apps-in-toss/web-framework': '@ait-co/devtools/mock',
|
|
97
|
+
'@apps-in-toss/web-bridge': '@ait-co/devtools/mock',
|
|
98
|
+
'@apps-in-toss/web-analytics': '@ait-co/devtools/mock',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
> **Panel injection**: Turbopack does not support unplugin, so the Panel is not auto-injected. Import it directly from your entry point:
|
|
106
|
+
> ```ts
|
|
107
|
+
> // app/layout.tsx or pages/_app.tsx
|
|
108
|
+
> import '@ait-co/devtools/panel';
|
|
109
|
+
> ```
|
|
110
|
+
|
|
111
|
+
### Next.js (Webpack)
|
|
112
|
+
|
|
113
|
+
When using Webpack mode in Next.js (`next dev` without `--turbo`, or `next build`):
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
// next.config.js (Webpack mode)
|
|
117
|
+
const aitDevtools = require('@ait-co/devtools/unplugin'); // CJS entrypoint provided
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
webpack: (config, { dev }) => {
|
|
121
|
+
if (dev) {
|
|
122
|
+
config.plugins.push(aitDevtools.webpack());
|
|
123
|
+
}
|
|
124
|
+
return config;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Manual alias setup
|
|
130
|
+
|
|
131
|
+
You can also configure the bundler's `resolve.alias` directly:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// vite.config.ts
|
|
135
|
+
import { defineConfig } from 'vite';
|
|
136
|
+
|
|
137
|
+
export default defineConfig({
|
|
138
|
+
resolve: {
|
|
139
|
+
alias: {
|
|
140
|
+
'@apps-in-toss/web-framework': '@ait-co/devtools/mock',
|
|
141
|
+
'@apps-in-toss/web-bridge': '@ait-co/devtools/mock',
|
|
142
|
+
'@apps-in-toss/web-analytics': '@ait-co/devtools/mock',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
// webpack.config.js (Webpack requires absolute paths)
|
|
150
|
+
module.exports = {
|
|
151
|
+
resolve: {
|
|
152
|
+
alias: {
|
|
153
|
+
'@apps-in-toss/web-framework': require.resolve('@ait-co/devtools/mock'),
|
|
154
|
+
'@apps-in-toss/web-bridge': require.resolve('@ait-co/devtools/mock'),
|
|
155
|
+
'@apps-in-toss/web-analytics': require.resolve('@ait-co/devtools/mock'),
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
> **Note**: Using manual aliases alone will not auto-inject the DevTools Panel. Add a direct import to your entry point:
|
|
162
|
+
> ```ts
|
|
163
|
+
> import '@ait-co/devtools/panel'; // add to entry point
|
|
164
|
+
> ```
|
|
165
|
+
|
|
166
|
+
### Plugin options
|
|
167
|
+
|
|
168
|
+
| Option | Type | Default | Description |
|
|
169
|
+
|---|---|---|---|
|
|
170
|
+
| `panel` | `boolean` | `true` | Auto-inject the DevTools Panel |
|
|
171
|
+
| `forceEnable` | `boolean` | `false` | Enable devtools even in production |
|
|
172
|
+
| `mock` | `boolean` | `true` (dev) / `false` (prod+forceEnable) | Enable mock alias |
|
|
173
|
+
| `tunnel` | `boolean \| { port?: number; qr?: boolean }` | `false` | Expose the Vite dev server via a Cloudflare quick tunnel for real-device preview (see [below](#run-on-a-real-phone)). **Vite dev mode only** |
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
aitDevtools.vite({ panel: false }); // mock only, no panel
|
|
177
|
+
aitDevtools.vite({ forceEnable: true }); // enable in production (mock OFF by default, panel ON)
|
|
178
|
+
aitDevtools.vite({ forceEnable: true, mock: true }); // enable mock in production too
|
|
179
|
+
aitDevtools.vite({ tunnel: true }); // expose dev server at *.trycloudflare.com
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Production builds
|
|
183
|
+
|
|
184
|
+
By default, the devtools plugin **automatically disables itself in production** (`NODE_ENV === 'production'` causes both the alias transform and the Panel injection to be skipped). No conditional configuration is needed to keep it safe.
|
|
185
|
+
|
|
186
|
+
To use devtools in a production build — for example in a staging environment — use the `forceEnable` option:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
aitDevtools.vite({ forceEnable: true }); // panel ON, mock OFF (monitoring only)
|
|
190
|
+
aitDevtools.vite({ forceEnable: true, mock: true }); // panel + mock both ON
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
You can also conditionally exclude the plugin from your bundler config entirely:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// vite.config.ts
|
|
197
|
+
import { defineConfig } from 'vite';
|
|
198
|
+
import aitDevtools from '@ait-co/devtools/unplugin';
|
|
199
|
+
|
|
200
|
+
export default defineConfig(({ command }) => ({
|
|
201
|
+
plugins: [
|
|
202
|
+
...(command === 'serve' ? [aitDevtools.vite()] : []),
|
|
203
|
+
],
|
|
204
|
+
}));
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
// webpack.config.js (same applies to Rspack)
|
|
209
|
+
const aitDevtools = require('@ait-co/devtools/unplugin');
|
|
210
|
+
const plugins = [];
|
|
211
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
212
|
+
plugins.push(aitDevtools.webpack());
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
> For Next.js, see the [Next.js (Webpack)](#nextjs-webpack) and [Next.js (Turbopack)](#nextjs-turbopack) sections above.
|
|
217
|
+
|
|
218
|
+
## Run on a real phone
|
|
219
|
+
|
|
220
|
+
When you want to view a mini-app that runs fine in desktop Chrome on an **actual phone**. The Vite dev server is exposed via a Cloudflare quick tunnel (`*.trycloudflare.com`, **no account required**), and you add a launcher PWA with a fixed URL to your phone's home screen once, then open each session's tunnel URL inside it.
|
|
221
|
+
|
|
222
|
+
Setup has three tiers:
|
|
223
|
+
|
|
224
|
+
- **Once per project** — add the option to `vite.config`, add the pnpm setting to `package.json`, and optionally add a `dev:phone` script
|
|
225
|
+
- **Once per phone** — add the launcher PWA to your home screen
|
|
226
|
+
- **Each session** — one line: `pnpm dev:phone` (or `AIT_TUNNEL=1 pnpm dev`)
|
|
227
|
+
|
|
228
|
+
### 1. Per-project setup
|
|
229
|
+
|
|
230
|
+
(a) **Add the `tunnel` option to `vite.config.ts`** — if you're fine with cloudflared starting every time, use `tunnel: true`; if you prefer to keep it off by default and enable it explicitly, use an env gate:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// vite.config.ts
|
|
234
|
+
import { defineConfig } from 'vite';
|
|
235
|
+
import aitDevtools from '@ait-co/devtools/unplugin';
|
|
236
|
+
|
|
237
|
+
export default defineConfig({
|
|
238
|
+
plugins: [
|
|
239
|
+
aitDevtools.vite({
|
|
240
|
+
tunnel: !!process.env.AIT_TUNNEL, // OFF by default, ON when AIT_TUNNEL=1
|
|
241
|
+
}),
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
> `process.env.AIT_TUNNEL` is evaluated when `vite.config.ts` is loaded (i.e. when the vite process starts). The env variable must therefore be set **before** vite launches (the `dev:phone` script in step (c) handles this automatically).
|
|
247
|
+
|
|
248
|
+
(b) **Allow the pnpm 10+ build script** — pnpm blocks dependency postinstall scripts by default for security. `cloudflared` downloads its binary (~38 MB) in postinstall, so you need to explicitly allow it:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"pnpm": {
|
|
253
|
+
"onlyBuiltDependencies": ["cloudflared"]
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
> Without this, things still work — `tunnel.ts` lazily calls `cloudflared.install()` on first start. You will just see an "Ignored build scripts" warning on every `pnpm install`, and the binary download is deferred to the first `pnpm dev`. See [`sdk-example#60`](https://github.com/apps-in-toss-community/sdk-example/pull/60).
|
|
259
|
+
|
|
260
|
+
(c) **(Optional) `dev:phone` script** — to avoid typing the env variable each time:
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"scripts": {
|
|
265
|
+
"dev": "vite",
|
|
266
|
+
"dev:phone": "AIT_TUNNEL=1 vite"
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 2. Per-phone setup (required)
|
|
272
|
+
|
|
273
|
+
Open `https://devtools.aitc.dev/launcher/` on your phone and **add it to your home screen** (iOS Safari: Share → Add to Home Screen; Android Chrome: Install app). The launcher URL never changes, so this is a one-time step.
|
|
274
|
+
|
|
275
|
+
The launcher **only works when launched as an installed PWA from the home screen**. Opening it in a regular browser tab shows only the install hint — the URL input and scanner are hidden. The chrome-less standalone display is the whole point of the launcher shell, and a regular tab can't provide that.
|
|
276
|
+
|
|
277
|
+
### 3. Each session
|
|
278
|
+
|
|
279
|
+
1. Run `pnpm dev:phone` on your desktop (or `AIT_TUNNEL=1 pnpm dev` if you skipped step 1-(c)). The terminal will print a `https://*.trycloudflare.com` URL along with an ASCII QR code.
|
|
280
|
+
2. Scan the QR code with your phone's camera (or with the "Scan QR" button inside the launcher). The QR encodes a `https://devtools.aitc.dev/launcher/?url=<tunnel>` deep-link, so the launcher PWA opens and auto-enters the day's dev app full-screen — no paste step required.
|
|
281
|
+
3. Next session, just scan the new QR. The launcher remembers the last URL and you can swap it any time with the "Rescan" button.
|
|
282
|
+
|
|
283
|
+
> Whether the OS camera routes the QR straight into the installed launcher PWA (instead of a regular browser tab) is most reliable on Android Chrome; iOS Safari versions may fall back to a normal tab. In that case, open the launcher from its home-screen icon and use its in-page "Scan QR" button.
|
|
284
|
+
|
|
285
|
+
### Background
|
|
286
|
+
|
|
287
|
+
> **Why go through a launcher?** The quick tunnel URL changes on every run, so installing that URL directly as a PWA gives you a dead link next session. Navigating cross-origin breaks the standalone (chrome-less) mode on both iOS and Android. → The solution is to install a launcher with a fixed URL once, and use an `<iframe>` inside it to show the day's dev app full-bleed.
|
|
288
|
+
>
|
|
289
|
+
> Quick tunnels have **no authentication**, the **URL changes on every run**, and they are **not for production use**. (If you have an account and domain, a named tunnel with a fixed hostname is possible via a future `tunnel: { hostname }` option.)
|
|
290
|
+
>
|
|
291
|
+
> The `tunnel` option only works in Vite dev mode — no tunnel is started for production builds, even with `forceEnable`. It is silently ignored for other bundlers (Webpack/Rspack, etc.). When the option is enabled, `cloudflared` and `qrcode-terminal` are loaded via dynamic import only, so they do not appear in the bundle graph when the option is off.
|
|
292
|
+
|
|
293
|
+
### One-line setup (planned)
|
|
294
|
+
|
|
295
|
+
The per-project steps above (vite.config patch + `onlyBuiltDependencies` + `dev:phone` script) are planned to be absorbed into a single command like `/ait setup phone` in the future [`agent-plugin`](https://github.com/apps-in-toss-community/agent-plugin) (command name is tentative). Since this README serves as the spec for that automation, the manual steps will remain documented here even after automation is available.
|
|
296
|
+
|
|
297
|
+
## Device API mode system
|
|
298
|
+
|
|
299
|
+
Device-related APIs (camera, location, clipboard, etc.) operate in three modes:
|
|
300
|
+
|
|
301
|
+
| Mode | Behavior | Use case |
|
|
302
|
+
|---|---|---|
|
|
303
|
+
| **mock** | Returns dummy data stored in `aitState` | Automated tests, fixed scenarios |
|
|
304
|
+
| **web** | Uses browser-native APIs (Geolocation, File API, etc.) | Testing with real device capabilities |
|
|
305
|
+
| **prompt** | DevTools Panel opens automatically and waits for user input (30-second timeout) | Manual QA, entering specific values |
|
|
306
|
+
|
|
307
|
+
### API support by mode
|
|
308
|
+
|
|
309
|
+
| API | mock | web | prompt |
|
|
310
|
+
|---|---|---|---|
|
|
311
|
+
| `openCamera` | ✅ | ✅ | ✅ |
|
|
312
|
+
| `fetchAlbumPhotos` | ✅ | ✅ | ✅ |
|
|
313
|
+
| `getCurrentLocation` | ✅ | ✅ | ✅ |
|
|
314
|
+
| `startUpdateLocation` | ✅ | ✅ | ✅ |
|
|
315
|
+
| `getNetworkStatus` | ✅ | ✅ | — |
|
|
316
|
+
| `getClipboardText` / `setClipboardText` | ✅ | ✅ | — |
|
|
317
|
+
|
|
318
|
+
### Setting the mode
|
|
319
|
+
|
|
320
|
+
```js
|
|
321
|
+
// Change individual API modes from the console
|
|
322
|
+
__ait.patch('deviceModes', { camera: 'web', location: 'prompt' });
|
|
323
|
+
|
|
324
|
+
// Or use the dropdown in the Device tab of the DevTools Panel
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Managing dummy images
|
|
328
|
+
|
|
329
|
+
Camera and album APIs return dummy images in mock mode.
|
|
330
|
+
|
|
331
|
+
- **Default placeholders**: 3 auto-generated 320×240 images in blue, green, and orange
|
|
332
|
+
- **Custom images**: Add or remove files from the Device tab in the DevTools Panel
|
|
333
|
+
- **Set from console**: `__ait.patch('mockData', { images: ['data:image/png;base64,...'] })`
|
|
334
|
+
|
|
335
|
+
## Floating DevTools Panel
|
|
336
|
+
|
|
337
|
+
When using the plugin, the panel is auto-injected into your entry point file. Click the **'AIT' button** in the bottom-right corner of the screen to toggle it.
|
|
338
|
+
|
|
339
|
+
### 10 tabs
|
|
340
|
+
|
|
341
|
+
| Tab | Description |
|
|
342
|
+
|---|---|
|
|
343
|
+
| **Environment** | Platform OS (ios/android), app version, environment (toss/sandbox), locale, network status, Safe Area Insets |
|
|
344
|
+
| **Presets** | Apply/remove common QA scenarios (permission denied, offline, logged out, etc.) with one click. Save and delete user presets |
|
|
345
|
+
| **Viewport** | Simulate a mobile viewport using device presets (iPhone/Galaxy) + orientation toggle |
|
|
346
|
+
| **Permissions** | Control camera, photos, geolocation, clipboard, contacts, and microphone permission states (allowed/denied/notDetermined) |
|
|
347
|
+
| **Location** | Set latitude, longitude, and accuracy |
|
|
348
|
+
| **Device** | Switch API modes (mock/web/prompt), manage dummy images (add/remove/reset to defaults) |
|
|
349
|
+
| **IAP** | Choose the next purchase result (success/cancel/error, etc.), TossPay payment result, completed order history (last 5) |
|
|
350
|
+
| **Events** | Trigger Back/Home navigation events, toggle login state |
|
|
351
|
+
| **Analytics** | Real-time log viewer for recorded analytics events (last 30 entries, with timestamp/type/parameters) |
|
|
352
|
+
| **Storage** | View and clear items stored via the `Storage` API |
|
|
353
|
+
|
|
354
|
+
> **Prompt mode auto-open**: When an API set to prompt mode is called, the Panel automatically opens the Device tab and shows the input UI.
|
|
355
|
+
|
|
356
|
+
### Mock state preset library (Presets tab)
|
|
357
|
+
|
|
358
|
+
When a scenario requires multiple mock keys to be in a specific state simultaneously (e.g. "IAP `NETWORK_ERROR` + payment fail when offline"), instead of setting them manually each time you can apply the whole set with one click. Applied presets show a ✓ indicator; if any key defined by the preset changes, the indicator automatically clears (keys not defined by the preset are not compared).
|
|
359
|
+
|
|
360
|
+
Built-in presets:
|
|
361
|
+
|
|
362
|
+
| ID | Meaning |
|
|
363
|
+
|---|---|
|
|
364
|
+
| `all-allowed` | All permissions allowed, WIFI, logged in, IAP success — return to baseline scenario |
|
|
365
|
+
| `permission-denied` | camera / photos / geolocation / contacts denied |
|
|
366
|
+
| `offline` | `getNetworkStatus` → OFFLINE, IAP `NETWORK_ERROR`, payment fail |
|
|
367
|
+
| `logged-out` | `auth.isLoggedIn=false`. Validates the login flow |
|
|
368
|
+
| `iap-pending` | IAP `nextResult` → `PAYMENT_PENDING` |
|
|
369
|
+
| `ads-no-fill` | Triggers the ad fill failure branch |
|
|
370
|
+
|
|
371
|
+
Any state you've toggled together can be saved as a preset via the "Save current as preset" button (persisted in `localStorage` with the `__ait_preset:<id>` prefix). Saved presets survive page reload and tab re-entry. Preset scope is limited to the `networkStatus / permissions / auth / iap / ads / payment` slices — unrelated state like viewport and brand is not affected.
|
|
372
|
+
|
|
373
|
+
Presets are also exported from the package:
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
import { applyPreset, builtInPresets, saveUserPreset } from '@ait-co/devtools';
|
|
377
|
+
|
|
378
|
+
// Apply a built-in preset
|
|
379
|
+
const offline = builtInPresets.find((p) => p.id === 'offline')!;
|
|
380
|
+
applyPreset(offline.state);
|
|
381
|
+
|
|
382
|
+
// Save a custom preset
|
|
383
|
+
saveUserPreset('My QA scenario', {
|
|
384
|
+
networkStatus: 'OFFLINE',
|
|
385
|
+
permissions: { camera: 'denied' },
|
|
386
|
+
auth: { isLoggedIn: false },
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Panel mount / dispose
|
|
391
|
+
|
|
392
|
+
Importing `@ait-co/devtools/panel` mounts the panel automatically when the DOM is ready. Mounting is idempotent — even if the same page imports it multiple times or calls `mount()` again, only one toggle button will be shown.
|
|
393
|
+
|
|
394
|
+
If you need to explicitly remove the panel in HMR or SPA routing scenarios, use `disposePanel()`:
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
import { disposePanel, mount } from '@ait-co/devtools/panel';
|
|
398
|
+
|
|
399
|
+
disposePanel(); // Removes the toggle, panel, injected <style>, and all listeners.
|
|
400
|
+
// Safe to call before mounting or to call twice.
|
|
401
|
+
mount(); // Re-mount from a clean state. No duplicate <style> or listeners.
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
`disposeViewport()` is called internally as well, so any active viewport simulation is also reverted.
|
|
405
|
+
|
|
406
|
+
## Device simulation (Viewport tab)
|
|
407
|
+
|
|
408
|
+
When developing mobile mini-apps in a desktop browser, you can validate layout against the actual device resolution, safe area, notch, home indicator, and Apps in Toss nav bar.
|
|
409
|
+
|
|
410
|
+
### Presets (2026)
|
|
411
|
+
|
|
412
|
+
| Category | Devices |
|
|
413
|
+
|---|---|
|
|
414
|
+
| Apple | iPhone SE (3rd gen), iPhone 16e, iPhone 17, iPhone Air, iPhone 17 Pro, iPhone 17 Pro Max |
|
|
415
|
+
| Samsung | Galaxy S26, S26+, S26 Ultra, Z Flip7, Z Fold7 (folded / unfolded) |
|
|
416
|
+
| Other | Custom (enter width/height manually), None (default) |
|
|
417
|
+
|
|
418
|
+
> **Galaxy S26 series** (released 2026-03-11): CSS viewport values use measurements from [phone-simulator.com](https://www.phone-simulator.com/). Safe area insets temporarily use S25 values pending real measurements in the Toss host environment — for pixel-accurate QA, verify on a real device.
|
|
419
|
+
>
|
|
420
|
+
> iPhone 17 series was released in September 2025 and is based on actual spec.
|
|
421
|
+
|
|
422
|
+
Each preset includes:
|
|
423
|
+
- **CSS viewport** (portrait `width × height`)
|
|
424
|
+
- **DPR** (devicePixelRatio: 2, 3, 3.5, etc.)
|
|
425
|
+
- **Notch** type (`none` / `notch` / `dynamic-island` / `punch-hole-center`)
|
|
426
|
+
- **OS-level safe area insets** (status bar / home indicator / left/right insets based on notch rotation)
|
|
427
|
+
|
|
428
|
+
### Orientation
|
|
429
|
+
|
|
430
|
+
- **auto** (default) — The Panel does not force any orientation. Calls to `setDeviceOrientation` from your app are recorded in a separate field (`appOrientation`) and used to determine the effective orientation. Repeated calls from the same app are always reflected correctly.
|
|
431
|
+
- **portrait / landscape** — The Panel overrides orientation. Calls to `setDeviceOrientation` from your app are ignored and logged with `console.warn`.
|
|
432
|
+
|
|
433
|
+
When switching to landscape:
|
|
434
|
+
- CSS viewport width and height are swapped.
|
|
435
|
+
- For iPhone (notch/Dynamic Island) presets, the safe area top becomes 0 and an inset appears on only one side depending on the **Notch side** toggle (left/right, default left) — matching real device behavior.
|
|
436
|
+
- For Android (punch-hole) presets, the status bar stays at the top.
|
|
437
|
+
|
|
438
|
+
### Frame + notch + home indicator + Apps in Toss nav bar
|
|
439
|
+
|
|
440
|
+
When **Show frame** is toggled on:
|
|
441
|
+
- Border-radius + box-shadow to mimic the device bezel
|
|
442
|
+
- Notch / Dynamic Island / punch-hole overlay (absolutely positioned at the top of body)
|
|
443
|
+
- Home indicator pill (only on devices with `safeAreaBottom > 0`, positioned at the bottom of body)
|
|
444
|
+
- App name uses `aitState.brand.displayName` (editable in the Environment tab, auto-updates)
|
|
445
|
+
- The back button triggers `__ait:backEvent` and the X button calls `closeView()` — you can verify actual SDK event plumbing directly from the panel
|
|
446
|
+
|
|
447
|
+
When **Show Apps in Toss nav bar** is toggled on (default on):
|
|
448
|
+
- A 48px nav bar overlay simulating the Toss host's top nav bar (back / app icon+name / ⋯ / ×)
|
|
449
|
+
- Positioned just below the status bar, after the safe area top
|
|
450
|
+
- **Important**: these 48px are **not included** in `env(safe-area-inset-top)` or `SafeAreaInsets.get().top` (this matches the SDK behavior). Toss-side examples compensate using the pattern `insets.top + 48`.
|
|
451
|
+
|
|
452
|
+
### Console manipulation
|
|
453
|
+
|
|
454
|
+
```js
|
|
455
|
+
// iPhone 17 Pro portrait + frame on
|
|
456
|
+
__ait.patch('viewport', { preset: 'iphone-17-pro', orientation: 'auto', frame: true });
|
|
457
|
+
|
|
458
|
+
// Force landscape (app's setDeviceOrientation calls are ignored)
|
|
459
|
+
__ait.patch('viewport', { orientation: 'landscape' });
|
|
460
|
+
|
|
461
|
+
// Notch side in landscape (iOS default 'left')
|
|
462
|
+
__ait.patch('viewport', { landscapeSide: 'right' });
|
|
463
|
+
|
|
464
|
+
// Custom size (automatically clamped to 1–4096)
|
|
465
|
+
__ait.patch('viewport', { preset: 'custom', customWidth: 360, customHeight: 740 });
|
|
466
|
+
|
|
467
|
+
// Hide the Apps in Toss nav bar (to inspect the pure viewport)
|
|
468
|
+
__ait.patch('viewport', { aitNavBar: false });
|
|
469
|
+
|
|
470
|
+
// Toggle nav bar variant ('partner' = white background + icon/name, 'game' = transparent + ⋯/× only)
|
|
471
|
+
__ait.patch('viewport', { aitNavBarType: 'game' });
|
|
472
|
+
|
|
473
|
+
// Reset
|
|
474
|
+
__ait.patch('viewport', { preset: 'none' });
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Status panel
|
|
478
|
+
|
|
479
|
+
The bottom of the Viewport tab shows the currently applied values in real time:
|
|
480
|
+
- **CSS / physical**: `402×874@3x | 1206×2622 portrait (auto)`
|
|
481
|
+
- **Safe area**: `T59 R0 B34 L0`
|
|
482
|
+
- **AIT nav bar**: `48px (excl. SafeArea)`
|
|
483
|
+
|
|
484
|
+
### Persistence + technical details
|
|
485
|
+
|
|
486
|
+
- State is saved to sessionStorage (`__ait_viewport`) and restored on page reload.
|
|
487
|
+
- Selecting a preset also updates `aitState.safeAreaInsets` → the SDK's `SafeAreaInsets.get()` / `.subscribe()` follow along.
|
|
488
|
+
- The viewport is applied to `document.body` via `max-width`/`max-height` + `margin:auto`. No iframe is used, so the app's JS/CSS runs as-is and DevTools remains fully accessible.
|
|
489
|
+
- `isolation: isolate` is applied to body so the z-index of the notch/nav bar/home indicator overlay doesn't leak outside the stacking context (the DevTools panel floats above).
|
|
490
|
+
- If you need to remove the viewport simulation programmatically, `disposeViewport()` is available as an export.
|
|
491
|
+
- User-Agent spoofing / touch event emulation / network throttling are not done (Chrome DevTools already provides these).
|
|
492
|
+
|
|
493
|
+
### Known limitations
|
|
494
|
+
|
|
495
|
+
- **Body becomes the scroll container** — while the viewport is active, scrolling happens on `document.body` rather than `window`. `window.addEventListener('scroll', ...)` or `IntersectionObserver` attached to the root may behave differently from a real device. If your mini-app handles scrolling, verify it against `body` as well.
|
|
496
|
+
- **Estimated presets** — iPhone Air is labeled `(est)` (not yet released) and will be updated when it ships. Galaxy S26 series is based on published spec (phone-simulator.com measurements), but safe area values are temporarily from S25 — pixel-accurate QA should be verified on a real device.
|
|
497
|
+
|
|
498
|
+
## `window.__ait` console API
|
|
499
|
+
|
|
500
|
+
You can control mock state directly from the browser console via `window.__ait` (or just `__ait`):
|
|
501
|
+
|
|
502
|
+
```js
|
|
503
|
+
// Read current state
|
|
504
|
+
__ait.state // full state object
|
|
505
|
+
__ait.state.platform // 'ios' or 'android'
|
|
506
|
+
__ait.state.auth.isLoggedIn // login state
|
|
507
|
+
__ait.state.deviceModes // current mode for each API
|
|
508
|
+
|
|
509
|
+
// Update state (shallow merge)
|
|
510
|
+
__ait.update({ platform: 'android', locale: 'en-US' });
|
|
511
|
+
__ait.update({ networkStatus: 'OFFLINE' });
|
|
512
|
+
|
|
513
|
+
// Update nested state
|
|
514
|
+
__ait.patch('permissions', { camera: 'denied' });
|
|
515
|
+
__ait.patch('deviceModes', { location: 'web' });
|
|
516
|
+
__ait.patch('iap', { nextResult: 'USER_CANCELED' });
|
|
517
|
+
|
|
518
|
+
// Trigger events
|
|
519
|
+
__ait.trigger('backEvent');
|
|
520
|
+
__ait.trigger('homeEvent');
|
|
521
|
+
|
|
522
|
+
// Log an analytics event manually
|
|
523
|
+
__ait.logAnalytics({ type: 'click', params: { button: 'purchase' } });
|
|
524
|
+
|
|
525
|
+
// Reset state (deviceId is preserved)
|
|
526
|
+
__ait.reset();
|
|
527
|
+
|
|
528
|
+
// Subscribe to state changes
|
|
529
|
+
const unsubscribe = __ait.subscribe(() => {
|
|
530
|
+
console.log('state changed:', __ait.state);
|
|
531
|
+
});
|
|
532
|
+
unsubscribe(); // unsubscribe
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
## Mock API reference
|
|
536
|
+
|
|
537
|
+
### Auth / login
|
|
538
|
+
|
|
539
|
+
| API | Mock behavior |
|
|
540
|
+
|---|---|
|
|
541
|
+
| `appLogin` | Returns `{ authorizationCode, referrer }` |
|
|
542
|
+
| `getIsTossLoginIntegratedService` | Returns state's `isTossLoginIntegrated` |
|
|
543
|
+
| `getUserKeyForGame` | Returns `{ hash, type: 'HASH' }` (or `undefined` when not logged in) |
|
|
544
|
+
| `appsInTossSignTossCert` | Console log only (no-op) |
|
|
545
|
+
|
|
546
|
+
### Screen / navigation
|
|
547
|
+
|
|
548
|
+
| API | Mock behavior |
|
|
549
|
+
|---|---|
|
|
550
|
+
| `closeView` | Calls `window.history.back()` |
|
|
551
|
+
| `openURL` | Opens in a new tab via `window.open()` |
|
|
552
|
+
| `share` | Uses `navigator.share()` (falls back to console log if unsupported) |
|
|
553
|
+
| `getTossShareLink` | Returns `https://toss.im/share/mock{path}` |
|
|
554
|
+
| `setIosSwipeGestureEnabled` | Console log (no-op) |
|
|
555
|
+
| `setDeviceOrientation` | Console log (no-op) |
|
|
556
|
+
| `setScreenAwakeMode` | Returns `{ enabled }` |
|
|
557
|
+
| `setSecureScreen` | Returns `{ enabled }` |
|
|
558
|
+
| `requestReview` | No-op (includes `.isSupported()` method) |
|
|
559
|
+
|
|
560
|
+
### Environment info
|
|
561
|
+
|
|
562
|
+
| API | Mock behavior |
|
|
563
|
+
|---|---|
|
|
564
|
+
| `getPlatformOS` | Returns state's platform (default: `'ios'`) |
|
|
565
|
+
| `getOperationalEnvironment` | Returns state's environment (default: `'sandbox'`) |
|
|
566
|
+
| `getTossAppVersion` | Returns state's appVersion (default: `'5.240.0'`) |
|
|
567
|
+
| `isMinVersionSupported` | Performs a semantic version comparison |
|
|
568
|
+
| `getSchemeUri` | Returns state's schemeUri or `window.location.pathname` |
|
|
569
|
+
| `getLocale` | Returns state's locale (default: `'ko-KR'`) |
|
|
570
|
+
| `getDeviceId` | Returns a persistent unique UUID stored in localStorage |
|
|
571
|
+
| `getGroupId` | Returns state's groupId |
|
|
572
|
+
| `getNetworkStatus` | Uses state or browser API depending on mode |
|
|
573
|
+
| `getServerTime` | Returns `Date.now()` |
|
|
574
|
+
| `env.getDeploymentId` | Returns state's deploymentId |
|
|
575
|
+
| `getAppsInTossGlobals` | Returns `{ deploymentId, brandDisplayName, brandIcon, brandPrimaryColor }` |
|
|
576
|
+
|
|
577
|
+
### Safe Area
|
|
578
|
+
|
|
579
|
+
| API | Mock behavior |
|
|
580
|
+
|---|---|
|
|
581
|
+
| `SafeAreaInsets.get` | Returns `{ top, bottom, left: 0, right: 0 }` |
|
|
582
|
+
| `SafeAreaInsets.subscribe` | Calls callback on state change, returns unsubscribe function |
|
|
583
|
+
| `getSafeAreaInsets` | Returns the top inset value (deprecated) |
|
|
584
|
+
|
|
585
|
+
### Device features
|
|
586
|
+
|
|
587
|
+
| API | Mock behavior |
|
|
588
|
+
|---|---|
|
|
589
|
+
| `Storage.getItem/setItem/removeItem/clearItems` | Stored in localStorage with `__ait_storage:` prefix |
|
|
590
|
+
| `getCurrentLocation` | Per mode: mock (state coordinates), web (Geolocation API), prompt (Panel input) |
|
|
591
|
+
| `startUpdateLocation` | mock (random coordinate variation), web (watchPosition), prompt (repeated input) |
|
|
592
|
+
| `openCamera` | mock (dummy image), web (file picker), prompt (Panel file input) |
|
|
593
|
+
| `fetchAlbumPhotos` | mock (dummy image array), web (multi-file select), prompt (Panel file input) |
|
|
594
|
+
| `fetchContacts` | Returns paginated mock contacts, supports `query.contains` search |
|
|
595
|
+
| `getClipboardText` / `setClipboardText` | mock (state storage) or web (Clipboard API) |
|
|
596
|
+
| `generateHapticFeedback` | Console log + analytics record |
|
|
597
|
+
| `saveBase64Data` | File download via anchor element |
|
|
598
|
+
|
|
599
|
+
### IAP / payments
|
|
600
|
+
|
|
601
|
+
| API | Mock behavior |
|
|
602
|
+
|---|---|
|
|
603
|
+
| `IAP.createOneTimePurchaseOrder` | Simulates success/failure after a 300ms delay based on state's `nextResult` |
|
|
604
|
+
| `IAP.createSubscriptionPurchaseOrder` | Same flow as above |
|
|
605
|
+
| `IAP.getProductItemList` | Returns state's product list |
|
|
606
|
+
| `IAP.getPendingOrders` | Returns pending order list |
|
|
607
|
+
| `IAP.getCompletedOrRefundedOrders` | Returns completed/refunded order list |
|
|
608
|
+
| `IAP.completeProductGrant` | Moves order from pending to completed |
|
|
609
|
+
| `IAP.getSubscriptionInfo` | Returns active subscription mock (30-day expiry, auto-renew) |
|
|
610
|
+
| `checkoutPayment` | Returns state's payment result after 300ms delay (TossPay) |
|
|
611
|
+
|
|
612
|
+
**IAP purchase simulation flow:**
|
|
613
|
+
|
|
614
|
+
1. `IAP.createOneTimePurchaseOrder()` called
|
|
615
|
+
2. 300ms delay (simulates payment UI)
|
|
616
|
+
3. Check `state.iap.nextResult` → if not `'success'`, call `onError`
|
|
617
|
+
4. On success, run the `processProductGrant` callback → on failure, return `'PRODUCT_NOT_GRANTED_BY_PARTNER'` error
|
|
618
|
+
5. On full success, record in `completedOrders` and deliver order result via `onEvent`
|
|
619
|
+
|
|
620
|
+
### Ads
|
|
621
|
+
|
|
622
|
+
| API | Mock behavior |
|
|
623
|
+
|---|---|
|
|
624
|
+
| `GoogleAdMob.loadAppsInTossAdMob` | Emits a `loaded` event after 200ms |
|
|
625
|
+
| `GoogleAdMob.showAppsInTossAdMob` | Sequentially emits requested→show→impression→reward→dismissed events over 50ms–1.5s |
|
|
626
|
+
| `GoogleAdMob.isAppsInTossAdMobLoaded` | Returns boolean loaded state |
|
|
627
|
+
| `TossAds.initialize/attach/attachBanner` | Renders a gray placeholder div |
|
|
628
|
+
| `TossAds.destroy/destroyAll` | No-op |
|
|
629
|
+
| `loadFullScreenAd` / `showFullScreenAd` | Similar flow to GoogleAdMob |
|
|
630
|
+
|
|
631
|
+
### Events
|
|
632
|
+
|
|
633
|
+
| API | Mock behavior |
|
|
634
|
+
|---|---|
|
|
635
|
+
| `graniteEvent.addEventListener` | Listens for `__ait:backEvent` and `__ait:homeEvent` custom events |
|
|
636
|
+
| `appsInTossEvent.addEventListener` | No-op |
|
|
637
|
+
| `tdsEvent.addEventListener` | Listens for `__ait:navigationAccessoryEvent` |
|
|
638
|
+
| `onVisibilityChangedByTransparentServiceWeb` | Delegates to `document.visibilitychange` event |
|
|
639
|
+
|
|
640
|
+
### Analytics
|
|
641
|
+
|
|
642
|
+
| API | Mock behavior |
|
|
643
|
+
|---|---|
|
|
644
|
+
| `Analytics.screen/impression/click` | Records by type in analyticsLog, viewable in the Panel in real time |
|
|
645
|
+
| `eventLog` | Records custom events by `log_name`, `log_type`, and `params` |
|
|
646
|
+
|
|
647
|
+
### Game / promotions
|
|
648
|
+
|
|
649
|
+
| API | Mock behavior |
|
|
650
|
+
|---|---|
|
|
651
|
+
| `grantPromotionReward` | Returns a timestamp-based mock key |
|
|
652
|
+
| `grantPromotionRewardForGame` | Same as above |
|
|
653
|
+
| `submitGameCenterLeaderBoardScore` | Appends score to state, returns `{ statusCode: 'SUCCESS' }` |
|
|
654
|
+
| `getGameCenterGameProfile` | Returns mock profile (or `PROFILE_NOT_FOUND` if absent) |
|
|
655
|
+
| `openGameCenterLeaderboard` | Console log (no-op) |
|
|
656
|
+
| `contactsViral` | Emits a close event after 500ms |
|
|
657
|
+
|
|
658
|
+
### Permissions
|
|
659
|
+
|
|
660
|
+
| API | Mock behavior |
|
|
661
|
+
|---|---|
|
|
662
|
+
| `getPermission` | Returns state's permission status (allowed/denied/notDetermined) |
|
|
663
|
+
| `openPermissionDialog` | Changes status to `allowed` |
|
|
664
|
+
| `requestPermission` | Delegates to `openPermissionDialog` |
|
|
665
|
+
|
|
666
|
+
> Functions that require permissions (openCamera, getCurrentLocation, etc.) are wrapped with `withPermission()`, which automatically attaches `.getPermission()` and `.openPermissionDialog()` methods.
|
|
667
|
+
|
|
668
|
+
### Partner
|
|
669
|
+
|
|
670
|
+
| API | Mock behavior |
|
|
671
|
+
|---|---|
|
|
672
|
+
| `partner.addAccessoryButton` | Console log (no-op) |
|
|
673
|
+
| `partner.removeAccessoryButton` | Console log (no-op) |
|
|
674
|
+
|
|
675
|
+
## Using in tests
|
|
676
|
+
|
|
677
|
+
You can import the mock library directly in vitest/jest.
|
|
678
|
+
|
|
679
|
+
> The mock functions use browser APIs such as `window`, `document`, and `localStorage`, so a **jsdom environment** is required.
|
|
680
|
+
>
|
|
681
|
+
> ```ts
|
|
682
|
+
> // vitest.config.ts
|
|
683
|
+
> import { defineConfig } from 'vitest/config';
|
|
684
|
+
> export default defineConfig({ test: { environment: 'jsdom' } });
|
|
685
|
+
> ```
|
|
686
|
+
|
|
687
|
+
```ts
|
|
688
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
689
|
+
import { appLogin, Storage, getCurrentLocation, getNetworkStatus, openCamera, IAP } from '@ait-co/devtools/mock';
|
|
690
|
+
import { aitState } from '@ait-co/devtools/mock';
|
|
691
|
+
|
|
692
|
+
beforeEach(() => {
|
|
693
|
+
aitState.reset(); // reset state before each test
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// Auth test
|
|
697
|
+
it('appLogin returns an authorizationCode', async () => {
|
|
698
|
+
const result = await appLogin();
|
|
699
|
+
expect(result.authorizationCode).toBeDefined();
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// Set state then call function
|
|
703
|
+
it('network status query when offline', async () => {
|
|
704
|
+
aitState.update({ networkStatus: 'OFFLINE' });
|
|
705
|
+
const status = await getNetworkStatus();
|
|
706
|
+
expect(status).toBe('OFFLINE');
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Permission denied scenario
|
|
710
|
+
it('throws when camera permission is denied', async () => {
|
|
711
|
+
aitState.patch('permissions', { camera: 'denied' });
|
|
712
|
+
await expect(openCamera()).rejects.toThrow();
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// IAP failure scenario (requires fake timers)
|
|
716
|
+
it('calls onError when purchase is canceled', async () => {
|
|
717
|
+
vi.useFakeTimers();
|
|
718
|
+
aitState.patch('iap', { nextResult: 'USER_CANCELED' });
|
|
719
|
+
const onError = vi.fn();
|
|
720
|
+
IAP.createOneTimePurchaseOrder({
|
|
721
|
+
options: { sku: 'item_01', processProductGrant: async () => true },
|
|
722
|
+
onEvent: vi.fn(),
|
|
723
|
+
onError,
|
|
724
|
+
});
|
|
725
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
726
|
+
expect(onError).toHaveBeenCalledWith({ code: 'USER_CANCELED' });
|
|
727
|
+
vi.useRealTimers();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Storage test
|
|
731
|
+
it('can write and read from Storage', async () => {
|
|
732
|
+
await Storage.setItem('key1', 'value1');
|
|
733
|
+
const result = await Storage.getItem('key1');
|
|
734
|
+
expect(result).toBe('value1');
|
|
735
|
+
});
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
## SDK update tracking
|
|
739
|
+
|
|
740
|
+
devtools tracks [`@apps-in-toss/web-framework`](https://www.npmjs.com/package/@apps-in-toss/web-framework), and [`sdk-example`](https://github.com/apps-in-toss-community/sdk-example) tracks both the original SDK and devtools. When a new SDK version is released, the flow is: (1) devtools catches up on mock/type signatures → (2) sdk-example incorporates both new versions together. If a devtools-only PR breaks sdk-example, both are addressed together.
|
|
741
|
+
|
|
742
|
+
Three mechanisms keep the SDK changes safely tracked:
|
|
743
|
+
|
|
744
|
+
### 1. Compile-time type verification (`__typecheck.ts`)
|
|
745
|
+
|
|
746
|
+
`src/__typecheck.ts` verifies that the major exports from the mock are type-compatible with the original SDK. If the SDK signature changes, `pnpm typecheck` will immediately produce an error.
|
|
747
|
+
|
|
748
|
+
```ts
|
|
749
|
+
type Assert<TMock, TOriginal> = TMock extends TOriginal ? true : never;
|
|
750
|
+
type _AppLogin = Assert<typeof Mock.appLogin, typeof Original.appLogin>;
|
|
751
|
+
// 40+ type compatibility assertions
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### 2. Proxy tripwire (runtime blocking)
|
|
755
|
+
|
|
756
|
+
`createMockProxy()` immediately throws an `Error` when an unimplemented API is accessed. This is intentional — to prevent "works in devtools but fails with the real SDK" production incidents caused by APIs that exist in the real SDK but haven't been mocked yet. Please [file an issue](https://github.com/apps-in-toss-community/devtools/issues) or add the mock yourself.
|
|
757
|
+
|
|
758
|
+
```
|
|
759
|
+
[@ait-co/devtools] IAP.newMethod is not mocked. This API may exist in
|
|
760
|
+
@apps-in-toss/web-framework, but devtools' mock does not cover it yet.
|
|
761
|
+
Please file an issue: https://github.com/apps-in-toss-community/devtools/issues
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### 3. Weekly GitHub Actions CI
|
|
765
|
+
|
|
766
|
+
`.github/workflows/check-sdk-update.yml` automatically runs **every Monday**:
|
|
767
|
+
|
|
768
|
+
1. Checks for a new version of `@apps-in-toss/web-framework`
|
|
769
|
+
2. Updates to the latest version and runs the type check
|
|
770
|
+
3. On detecting a new version, automatically opens a GitHub Issue (including whether there are type errors)
|
|
771
|
+
|
|
772
|
+
## Contributing
|
|
773
|
+
|
|
774
|
+
### Adding a new API mock
|
|
775
|
+
|
|
776
|
+
1. Implement the function in the appropriate category directory (e.g. `src/mock/device/`)
|
|
777
|
+
2. Add the export to `src/mock/index.ts`
|
|
778
|
+
3. Add a type compatibility assertion to `src/__typecheck.ts`
|
|
779
|
+
4. Run `pnpm typecheck` to verify compatibility with the original
|
|
780
|
+
5. Write tests in `src/__tests__/`
|
|
781
|
+
|
|
782
|
+
```bash
|
|
783
|
+
pnpm build # build with tsup
|
|
784
|
+
pnpm typecheck # verify type compatibility
|
|
785
|
+
pnpm test # run all tests
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### Pre-commit hook (optional)
|
|
789
|
+
|
|
790
|
+
Optional but recommended. After cloning, activate the standard pre-commit hook with the command below. It runs `biome check` automatically on staged files.
|
|
791
|
+
|
|
792
|
+
```sh
|
|
793
|
+
git config core.hooksPath .githooks
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
This hook is a developer convenience for catching lint issues before push. The actual enforcement layer is the CI `pnpm lint` job, so contributors who don't activate the hook will still see lint failures in their PR.
|
|
797
|
+
|
|
798
|
+
## Troubleshooting
|
|
799
|
+
|
|
800
|
+
### `[@ait-co/devtools] XXX.method is not mocked` error
|
|
801
|
+
|
|
802
|
+
The SDK API you're calling has not been implemented in the mock yet. devtools throws on unimplemented API access to prevent "works fine" deployments. [File an issue](https://github.com/apps-in-toss-community/devtools/issues) or add the mock yourself and try again.
|
|
803
|
+
|
|
804
|
+
### DevTools Panel not appearing
|
|
805
|
+
|
|
806
|
+
- Check that you haven't set `panel: false` in your plugin options
|
|
807
|
+
- If you're using manual alias setup, add a direct import to your entry point:
|
|
808
|
+
```ts
|
|
809
|
+
import '@ait-co/devtools/panel';
|
|
810
|
+
```
|
|
811
|
+
- The plugin auto-injects only into entry points whose filename is `main`, `index`, `entry`, or `app` (case-insensitive). If your filename doesn't match that pattern, add `import '@ait-co/devtools/panel'` manually.
|
|
812
|
+
|
|
813
|
+
### Subpath imports are not mocked
|
|
814
|
+
|
|
815
|
+
Subpath imports of the form `@apps-in-toss/web-framework/some-subpath` are not aliased. Only the main entry (`@apps-in-toss/web-framework`) is mocked. If you need a specific subpath mocked as well, add it manually to your bundler's `resolve.alias`.
|
|
816
|
+
|
|
817
|
+
### Setting up with Next.js Turbopack
|
|
818
|
+
|
|
819
|
+
Since Turbopack doesn't support unplugin, use `resolveAlias` in `next.config.js` (see the [Next.js (Turbopack)](#nextjs-turbopack) section above). Import the Panel directly from your entry point:
|
|
820
|
+
|
|
821
|
+
```ts
|
|
822
|
+
// app/layout.tsx or pages/_app.tsx
|
|
823
|
+
import '@ait-co/devtools/panel';
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
## Package export structure
|
|
827
|
+
|
|
828
|
+
| Import path | Purpose |
|
|
829
|
+
|---|---|
|
|
830
|
+
| `@ait-co/devtools` or `@ait-co/devtools/mock` | All mock exports (bundler alias target) |
|
|
831
|
+
| `@ait-co/devtools/panel` | Floating DevTools Panel (auto-mounts on import) |
|
|
832
|
+
| `@ait-co/devtools/unplugin` | Bundler plugin (.vite, .webpack, .rspack, .esbuild, .rollup) |
|
|
833
|
+
|
|
834
|
+
## Telemetry
|
|
835
|
+
|
|
836
|
+
devtools uses a two-tier telemetry model.
|
|
837
|
+
|
|
838
|
+
### Tier 0 — anonymous usage signal (ON by default, opt-out)
|
|
839
|
+
|
|
840
|
+
Sends a one-time anonymous ping per calendar day when the panel is opened.
|
|
841
|
+
|
|
842
|
+
Collected fields: `source`, `version`, `ts` — no PII, no `anon_id`. The server generates an IP+UA daily hash but never stores it.
|
|
843
|
+
|
|
844
|
+
How to opt out:
|
|
845
|
+
- Panel Environment tab → "Anonymous usage signal (Tier 0)" toggle OFF
|
|
846
|
+
- `localStorage.setItem('__ait_telemetry:t0_off', '1')` (from the browser console)
|
|
847
|
+
- Environment variable: `AITC_TELEMETRY=off`
|
|
848
|
+
|
|
849
|
+
### Tier 1 — extended telemetry (OFF by default, opt-in)
|
|
850
|
+
|
|
851
|
+
A consent toast appears on first panel use. Data is only collected if you accept.
|
|
852
|
+
|
|
853
|
+
Collected fields: `panel_open`, `tab_view`, `session_duration` events + an anonymous UUID (`anon_id`).
|
|
854
|
+
|
|
855
|
+
How to opt out:
|
|
856
|
+
- Panel Environment tab → "Extended telemetry (Tier 1)" toggle OFF
|
|
857
|
+
- Delete collected data: Panel Environment tab → "Delete my data"
|
|
858
|
+
|
|
859
|
+
Privacy policy: <https://docs.aitc.dev/privacy>
|
|
860
|
+
|
|
861
|
+
## License
|
|
862
|
+
|
|
863
|
+
BSD 3-Clause
|
|
864
|
+
|
|
865
|
+
---
|
|
866
|
+
|
|
867
|
+
Community open-source project.
|