web-launch-kit 0.0.7 → 0.0.9

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.ko.md CHANGED
@@ -33,7 +33,8 @@ npm install web-launch-kit
33
33
  | `LaunchKit.map(options?)` | `Promise<AppOpenedBy>` | 검색어·좌표·경로로 지도 열기. OS별 네이티브 앱과 Google Maps 웹 폴백 |
34
34
  | `LaunchKit.filepicker(options?)` | `Promise<File[]>` | 파일 또는 디렉터리 선택 (File System Access API, input 폴백) |
35
35
  | `LaunchKit.setting(type?)` | `Promise<void>` | 지원되는 환경에서 시스템 설정 화면 열기 |
36
- | `LaunchKit.utils` | object | `canOpenIntent` / `canOpenUniversal` / `canOpenSetting` getter와 비동기 `getTrackId` / `getProductId` |
36
+ | `LaunchKit.escapeWebview(options?)` | `Promise<void>` | URL(기본값은 현재 페이지)을 외부 브라우저에서 다시 열어 인앱 웹뷰에서 탈출 |
37
+ | `LaunchKit.utils` | object | `canOpenIntent` / `canOpenUniversal` / `canOpenSetting` / `canEscapeWebview` getter와 비동기 `getTrackId(bundleId)` / `getProductId(packageFamilyName, language?, country?)` |
37
38
 
38
39
  `AppOpenedBy`는 `"scheme"`, `"universal"`, `"intent"`, `"fallback"`, `"store"` 중 하나입니다.
39
40
 
@@ -82,8 +83,8 @@ LaunchKit.telephone({ to: '+821012345678' })
82
83
 
83
84
  싱글톤의 형태는 `LaunchKitInstance`로 export되고, 모든 옵션 객체에 이름이 붙어 있습니다 —
84
85
  `AppOpenOptions`, `AndroidAppInfo`, `IOSAppInfo`, `WindowsAppInfo`, `MacOSAppInfo`,
85
- `TelephoneOptions`, `MessageOptions`, `MailOptions`, `MapOptions`, `FilepickerOptions`.
86
- `SettingType`은 값 export이고 `AppOpenedBy`는 타입입니다.
86
+ `TelephoneOptions`, `MessageOptions`, `MailOptions`, `MapOptions`, `FilepickerOptions`,
87
+ `EscapeWebviewOptions`. `SettingType`은 값 export이고 `AppOpenedBy`는 타입입니다.
87
88
 
88
89
  ```ts
89
90
  import LaunchKit, {
@@ -146,6 +147,18 @@ flowchart TD
146
147
  M2 --> M3["Priority list:<br/>scheme ⭢ fallback<br/>⭢ app store ⭢ web store (by trackId)"]
147
148
  end
148
149
 
150
+ subgraph LOC["web store · createWebStoreURL"]
151
+ L1{"country given?"}
152
+ L1 -->|yes| L3["Play · MS Store: hl / gl parameters<br/>App Store: /country/ path + l parameter"]
153
+ L1 -->|no| L2["Derive from language:<br/>region subtag ⭢ Intl.Locale.maximize()<br/>⭢ omit (store geolocation)"]
154
+ L2 --> L3
155
+ end
156
+
157
+ A5 -.->|"web store"| L1
158
+ I3 -.->|"web store"| L1
159
+ W3 -.->|"web store"| L1
160
+ M3 -.->|"web store"| L1
161
+
149
162
  AND --> P
150
163
  IOS --> P
151
164
  WIN --> P
@@ -190,6 +203,8 @@ const openedBy = await LaunchKit.app({
190
203
  packageName: 'com.example.myapp',
191
204
  allowAppStore: true,
192
205
  allowWebStore: true,
206
+ language: 'ko', // 웹스토어 UI 언어
207
+ country: 'KR', // 생략하면 language 에서 유도됩니다 (region 서브태그 → Intl.Locale.maximize())
193
208
  },
194
209
  ios: {
195
210
  universal: 'https://example.com/profile/42',
@@ -205,7 +220,17 @@ console.log(openedBy) // "universal" | "scheme" | "intent" | "fallback" | "store
205
220
  플랫폼별 필드: 안드로이드는 `intent` / `scheme` / `packageName` / `fallback` /
206
221
  `allowIntent`(scheme ⇄ intent는 서로에게서 유도됩니다), iOS는 `universal` / `scheme` / `bundleId` / `trackId`, Windows는
207
222
  `scheme` / `packageFamilyName` / `productId`, macOS는 `scheme` / `bundleId` / `trackId`를
208
- 받습니다. 모두 `fallback`, `timeout`, `allowAppStore`, `allowWebStore`를 함께 받습니다.
223
+ 받습니다. 모두 `fallback`, `timeout`, `allowAppStore`, `allowWebStore`, `language`,
224
+ `country`를 함께 받습니다.
225
+
226
+ `language` / `country`는 **웹스토어** 후보에만 적용됩니다 — Google Play와 Microsoft
227
+ Store에서는 `hl` / `gl` 파라미터, App Store에서는 국가 경로 세그먼트와 `l` 파라미터로
228
+ 반영됩니다. `country`를 생략하면 `language`에서 유도합니다. region 서브태그가 우선하고
229
+ (`ko-KR` → `KR`), 없으면 런타임이 지원하는 환경에서 `Intl.Locale.maximize()`를
230
+ 사용합니다. 둘 다 실패하면 국가를 붙이지 않고 스토어의 자체 위치 추정에 맡깁니다 —
231
+ 잘못된 국가는 "이 지역에서 사용할 수 없음" 페이지로 이어질 수 있어 생략이 안전한
232
+ 기본값입니다. 네이티브 앱스토어 스킴(`market://`, `itms-apps://` 등)은 건드리지
233
+ 않습니다. OS가 기기 로케일로 알아서 렌더링합니다.
209
234
 
210
235
  `onAttempt`는 각 후보 시도 직전에 동기로 호출됩니다. 대기 UI를 여기서 그리세요 — 그러지
211
236
  않으면 타임아웃 동안 화면에 아무 변화가 없습니다.
@@ -313,6 +338,32 @@ if (LaunchKit.utils.canOpenSetting) {
313
338
  }
314
339
  ```
315
340
 
341
+ ## 인앱 브라우저 탈출
342
+
343
+ `escapeWebview()`는 URL — 기본값은 현재 페이지 — 을 외부 기본 브라우저에서 다시 열어,
344
+ 페이지가 갇혀 있는 인앱 웹뷰에서 탈출합니다.
345
+
346
+ ```js
347
+ import LaunchKit from 'web-launch-kit'
348
+
349
+ if (LaunchKit.utils.canEscapeWebview) {
350
+ await LaunchKit.escapeWebview()
351
+ }
352
+
353
+ // 현재 페이지 대신 특정 URL을 내보낼 수도 있습니다
354
+ await LaunchKit.escapeWebview({ url: 'https://example.com/checkout' })
355
+ ```
356
+
357
+ 경로는 환경에 따라 결정됩니다. 카카오톡은 전용 `kakaotalk://web/openExternal` 스킴을
358
+ 제공하고, LINE은 공식 문서화된 `openExternalBrowser=1` 쿼리 파라미터를 지원합니다 —
359
+ 둘 다 안드로이드와 iOS에서 동작합니다. 그 외 웹뷰는 OS 경로로 폴백합니다. 안드로이드는
360
+ Chrome을 고정한 `intent://` URL(원본 URL이 `S.browser_fallback_url`로 실려서 Chrome이
361
+ 없는 기기는 제자리에 머뭅니다), iOS는 비공개 `x-safari-` 스킴입니다 — iOS 15와 17+에서
362
+ 동작하고 16에서는 동작하지 않으며, 기본 브라우저 설정과 무관하게 항상 Safari를 열고,
363
+ Meta 계열 웹뷰(Facebook, Instagram) 안에서는 차단됩니다. http(s) URL만 다시 열 수
364
+ 있습니다. 웹뷰 밖이거나 사용 가능한 경로가 없으면(완전 차단 웹뷰, iOS 16, 데스크톱)
365
+ 프로미스가 reject됩니다 — 버튼 노출은 `utils.canEscapeWebview`로 게이트하세요.
366
+
316
367
  ---
317
368
 
318
369
  ## 참고
@@ -326,7 +377,12 @@ if (LaunchKit.utils.canOpenSetting) {
326
377
  - **`utils.getTrackId` / `getProductId`는 비동기**이고 1시간 캐시됩니다. 페이지 초기화 때
327
378
  한 번 호출해두면 `app()`이 id를 인라인으로 조회하지 않습니다. `app()`은 사용자 제스처 안에서
328
379
  실행돼야 하므로, id가 없으면 `await`으로 user activation을 잃는 대신 동기로 조회합니다.
329
- `trackId` / `productId`를 직접 주면 조회 자체가 생략됩니다.
380
+ `trackId` / `productId`를 직접 주면 조회 자체가 생략됩니다. `getProductId`는 선택적
381
+ `language` / `country`를 추가로 받아 Microsoft 카탈로그의 언어와 마켓을 고정합니다 —
382
+ 조회 성공 여부가 마켓에 따라 달라질 수 있으니 `app()`에 넘길 값과 동일하게 주어야
383
+ 프리워밍된 캐시가 같은 마켓을 커버합니다. 생략하면 웹스토어 country와 같은 방식으로
384
+ 유도됩니다: 명시 region 서브태그 → 런타임이 지원하면 `Intl.Locale.maximize()` →
385
+ `en-US` / `US`. `getTrackId`는 로케일이 필요 없습니다 — iTunes lookup은 마켓과 무관합니다.
330
386
  - **스토어 id 조회는 외부 API에 의존합니다**(iTunes Lookup, Microsoft display catalog).
331
387
  1시간 캐시되며 실패는 예외가 아니라 `undefined`로 resolve됩니다.
332
388
  - **인앱 브라우저는 후보 단위로 걸러집니다.** 실행이 차단된 웹뷰(WeChat, QQ, Qzone, Baidu,
package/README.md CHANGED
@@ -34,7 +34,8 @@ The bundle is self-contained (OS/locale detection is inlined) — no peer script
34
34
  | `LaunchKit.map(options?)` | `Promise<AppOpenedBy>` | Open a map by query, coordinate, or directions; native app per-OS with a Google Maps web fallback |
35
35
  | `LaunchKit.filepicker(options?)` | `Promise<File[]>` | Pick files or a directory (File System Access API, with input fallback) |
36
36
  | `LaunchKit.setting(type?)` | `Promise<void>` | Open a system-settings pane where supported |
37
- | `LaunchKit.utils` | object | `canOpenIntent` / `canOpenUniversal` / `canOpenSetting` getters, plus async `getTrackId` / `getProductId` |
37
+ | `LaunchKit.escapeWebview(options?)` | `Promise<void>` | Reopen a URL (the current page by default) in the external browser, escaping an in-app webview |
38
+ | `LaunchKit.utils` | object | `canOpenIntent` / `canOpenUniversal` / `canOpenSetting` / `canEscapeWebview` getters, plus async `getTrackId(bundleId)` / `getProductId(packageFamilyName, language?, country?)` |
38
39
 
39
40
  `AppOpenedBy` is one of: `"scheme"`, `"universal"`, `"intent"`, `"fallback"`, `"store"`.
40
41
 
@@ -84,7 +85,8 @@ The global `LaunchKit` is a namespace object; the singleton is `LaunchKit.defaul
84
85
  The singleton shape is exported as `LaunchKitInstance`, and every option object has a
85
86
  named type: `AppOpenOptions`, `AndroidAppInfo`, `IOSAppInfo`, `WindowsAppInfo`,
86
87
  `MacOSAppInfo`, `TelephoneOptions`, `MessageOptions`, `MailOptions`, `MapOptions`,
87
- `FilepickerOptions`. `SettingType` is a value export; `AppOpenedBy` is a type.
88
+ `FilepickerOptions`, `EscapeWebviewOptions`. `SettingType` is a value export;
89
+ `AppOpenedBy` is a type.
88
90
 
89
91
  ```ts
90
92
  import LaunchKit, {
@@ -147,6 +149,18 @@ flowchart TD
147
149
  M2 --> M3["Priority list:<br/>scheme ⭢ fallback<br/>⭢ app store ⭢ web store (by trackId)"]
148
150
  end
149
151
 
152
+ subgraph LOC["web store · createWebStoreURL"]
153
+ L1{"country given?"}
154
+ L1 -->|yes| L3["Play · MS Store: hl / gl parameters<br/>App Store: /country/ path + l parameter"]
155
+ L1 -->|no| L2["Derive from language:<br/>region subtag ⭢ Intl.Locale.maximize()<br/>⭢ omit (store geolocation)"]
156
+ L2 --> L3
157
+ end
158
+
159
+ A5 -.->|"web store"| L1
160
+ I3 -.->|"web store"| L1
161
+ W3 -.->|"web store"| L1
162
+ M3 -.->|"web store"| L1
163
+
150
164
  AND --> P
151
165
  IOS --> P
152
166
  WIN --> P
@@ -191,6 +205,8 @@ const openedBy = await LaunchKit.app({
191
205
  packageName: 'com.example.myapp',
192
206
  allowAppStore: true,
193
207
  allowWebStore: true,
208
+ language: 'ko', // web-store UI language
209
+ country: 'KR', // omit to derive from language (region subtag → Intl.Locale.maximize())
194
210
  },
195
211
  ios: {
196
212
  universal: 'https://example.com/profile/42',
@@ -207,7 +223,17 @@ Per-platform fields: Android accepts `intent` / `scheme` / `packageName` / `fall
207
223
  `allowIntent` (scheme ⇄ intent are derived from each other); iOS accepts `universal` / `scheme` /
208
224
  `bundleId` / `trackId`; Windows accepts `scheme` / `packageFamilyName` / `productId`;
209
225
  macOS accepts `scheme` / `bundleId` / `trackId`. All accept `fallback`, `timeout`,
210
- `allowAppStore`, `allowWebStore`.
226
+ `allowAppStore`, `allowWebStore`, `language`, `country`.
227
+
228
+ `language` / `country` localize the **web-store** candidate only — `hl` / `gl` on
229
+ Google Play and the Microsoft Store, a country path segment plus `l` on the App
230
+ Store. When `country` is omitted it is derived from `language`: a region subtag
231
+ wins (`ko-KR` → `KR`), then `Intl.Locale.maximize()` where the runtime supports
232
+ it. When neither yields a region, the country is simply dropped and the store
233
+ falls back to its own geolocation — a wrong country can land on an
234
+ "not available in your region" page, so omission is the safe default. Native
235
+ app-store schemes (`market://`, `itms-apps://`, …) are untouched; the OS renders
236
+ those in the device locale.
211
237
 
212
238
  `onAttempt` fires synchronously before each candidate, which is where you drive the waiting
213
239
  UI — there is otherwise nothing on screen for the length of the timeout.
@@ -316,6 +342,34 @@ if (LaunchKit.utils.canOpenSetting) {
316
342
  }
317
343
  ```
318
344
 
345
+ ## Escaping in-app browsers
346
+
347
+ `escapeWebview()` reopens a URL — the current page by default — in the external
348
+ default browser, escaping the in-app webview the page is trapped in.
349
+
350
+ ```js
351
+ import LaunchKit from 'web-launch-kit'
352
+
353
+ if (LaunchKit.utils.canEscapeWebview) {
354
+ await LaunchKit.escapeWebview()
355
+ }
356
+
357
+ // Or send a specific URL out instead of the current page
358
+ await LaunchKit.escapeWebview({ url: 'https://example.com/checkout' })
359
+ ```
360
+
361
+ The route depends on the environment. KakaoTalk exposes a dedicated
362
+ `kakaotalk://web/openExternal` scheme and LINE honors the documented
363
+ `openExternalBrowser=1` query parameter — both work on Android and iOS. Other
364
+ webviews fall back to the OS route: a Chrome-pinned `intent://` URL on Android
365
+ (the original URL rides along as `S.browser_fallback_url`, so a Chrome-less
366
+ device stays in place), and the undocumented `x-safari-` scheme on iOS — which
367
+ works on iOS 15 and 17+ but not 16, always opens Safari regardless of the
368
+ default browser, and is blocked inside Meta's webviews (Facebook, Instagram).
369
+ Only http(s) URLs can be reopened. Outside a webview, or where no route is
370
+ available (hard-blocked webviews, iOS 16, desktop), the promise rejects — gate
371
+ the button on `utils.canEscapeWebview`.
372
+
319
373
  ---
320
374
 
321
375
  ## Notes
@@ -330,6 +384,12 @@ if (LaunchKit.utils.canOpenSetting) {
330
384
  during page setup keeps `app()` from resolving the id inline. `app()` must run inside a
331
385
  user gesture, so when the id is missing it looks it up synchronously rather than awaiting
332
386
  and losing user activation. Passing `trackId` / `productId` directly skips it entirely.
387
+ `getProductId` also takes optional `language` / `country` to pin the Microsoft catalog
388
+ language and market — pass the same values you will give `app()`, since whether a lookup
389
+ succeeds can depend on the market and the pre-warmed cache should cover the same one.
390
+ When omitted, the market is derived the same way as the web-store country: explicit region
391
+ subtag first, then `Intl.Locale.maximize()` where the runtime supports it, then `en-US` / `US`.
392
+ `getTrackId` needs no locale — iTunes lookup is market-independent.
333
393
  - **Store-id lookups depend on remote APIs** (iTunes Lookup, Microsoft display catalog)
334
394
  and are cached for one hour; failures resolve to `undefined` rather than throwing.
335
395
  - **In-app browsers are gated per candidate.** Inside webviews that block launches
package/dist/index.d.ts CHANGED
@@ -32,6 +32,8 @@ declare interface AppInfo {
32
32
  timeout?: number;
33
33
  allowAppStore?: boolean;
34
34
  allowWebStore?: boolean;
35
+ language?: string;
36
+ country?: string;
35
37
  }
36
38
  declare interface AndroidAppInfo extends AppInfo {
37
39
  intent?: URLCandidate;
@@ -93,12 +95,16 @@ declare interface MapOptions {
93
95
  fallback?: URLStringOrFallback;
94
96
  onAttempt?: (attempt: LaunchAttempt) => void;
95
97
  }
98
+ declare interface EscapeWebviewOptions {
99
+ url?: URLCandidate;
100
+ }
96
101
  interface LaunchKitUtils {
97
102
  get canOpenIntent(): boolean;
98
103
  get canOpenUniversal(): boolean;
99
104
  get canOpenSetting(): boolean;
105
+ get canEscapeWebview(): boolean;
100
106
  getTrackId(bundleId: string): Promise<string | undefined>;
101
- getProductId(packageFamilyName: string): Promise<string | undefined>;
107
+ getProductId(packageFamilyName: string, language?: string, country?: string): Promise<string | undefined>;
102
108
  }
103
109
  interface LaunchKitInstance {
104
110
  readonly version: string;
@@ -113,8 +119,9 @@ interface LaunchKitInstance {
113
119
  map(options?: MapOptions): Promise<AppOpenedBy>;
114
120
  filepicker(options?: FilepickerOptions): Promise<File[]>;
115
121
  setting(type?: SettingType): Promise<void>;
122
+ escapeWebview(options?: EscapeWebviewOptions): Promise<void>;
116
123
  }
117
124
  declare const LaunchKit: LaunchKitInstance;
118
125
 
119
126
  export { SettingType, LaunchKit as default };
120
- export type { AndroidAppInfo, AppInfo, AppOpenOptions, AppOpenedBy, FilepickerOptions, IOSAppInfo, LaunchAttempt, LaunchError, LaunchErrorCode, LaunchKitInstance, MacOSAppInfo, MailOptions, MapOptions, MessageOptions, OpenPickerStartIn, TelephoneOptions, WindowsAppInfo };
127
+ export type { AndroidAppInfo, AppInfo, AppOpenOptions, AppOpenedBy, EscapeWebviewOptions, FilepickerOptions, IOSAppInfo, LaunchAttempt, LaunchError, LaunchErrorCode, LaunchKitInstance, MacOSAppInfo, MailOptions, MapOptions, MessageOptions, OpenPickerStartIn, TelephoneOptions, WindowsAppInfo };