web-launch-kit 0.0.4 → 0.0.6

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
@@ -24,18 +24,26 @@ npm install web-launch-kit
24
24
  | 멤버 | 시그니처 | 설명 |
25
25
  | --- | --- | --- |
26
26
  | `LaunchKit.version` | `string` | 설치된 패키지 버전 |
27
+ | `LaunchKit.debug` | `boolean` (get/set) | 각 실행 시도 전에 `debugger`로 정지. 기본값 `false` |
27
28
  | `LaunchKit.SettingType` | `enum` | 설정 화면: `General`, `Network`, `Display`, `Appearance`, `Accessibility`, `Battery`, `Datetime`, `Language`, `Accounts`, `Storage` |
28
29
  | `LaunchKit.app(options?)` | `Promise<AppOpenedBy>` | 가능한 최선의 경로로 앱을 실행하고, 어떤 경로로 열렸는지 반환 |
29
30
  | `LaunchKit.telephone(options?)` | `Promise<void>` | 전화 앱 열기 (`tel:`) |
30
31
  | `LaunchKit.message(options?)` | `Promise<void>` | 문자 작성 열기 (`sms:`) |
31
32
  | `LaunchKit.mail(options?)` | `Promise<void>` | 메일 작성 열기 (`mailto:`) |
32
- | `LaunchKit.map(options?)` | `Promise<void>` | 검색어·좌표·경로로 지도 열기. OS별 네이티브 앱과 Google Maps 웹 폴백 |
33
+ | `LaunchKit.map(options?)` | `Promise<AppOpenedBy>` | 검색어·좌표·경로로 지도 열기. OS별 네이티브 앱과 Google Maps 웹 폴백 |
33
34
  | `LaunchKit.filepicker(options?)` | `Promise<File[]>` | 파일 또는 디렉터리 선택 (File System Access API, input 폴백) |
34
35
  | `LaunchKit.setting(type?)` | `Promise<void>` | 지원되는 환경에서 시스템 설정 화면 열기 |
35
36
  | `LaunchKit.utils` | object | `canOpenIntent` / `canOpenUniversal` / `canOpenSetting` getter와 비동기 `getTrackId` / `getProductId` |
36
37
 
37
38
  `AppOpenedBy`는 `"scheme"`, `"universal"`, `"intent"`, `"fallback"`, `"store"` 중 하나입니다.
38
39
 
40
+ `app()`과 `map()`은 `LaunchError`로 reject합니다 — 필드 두 개가 붙은 실제 `Error`입니다.
41
+
42
+ | 필드 | 타입 | 설명 |
43
+ | --- | --- | --- |
44
+ | `code` | `"unsupported-os" \| "no-candidates" \| "all-failed"` | 실패 사유 |
45
+ | `attempted` | `readonly LaunchAttempt[]` | 시도한 모든 후보의 `{ by, url, index, total }` |
46
+
39
47
  ---
40
48
 
41
49
  ## ESM
@@ -199,6 +207,21 @@ console.log(openedBy) // "universal" | "scheme" | "intent" | "fallback" | "store
199
207
  `scheme` / `packageFamilyName` / `productId`, macOS는 `scheme` / `bundleId` / `trackId`를
200
208
  받습니다. 모두 `fallback`, `timeout`, `allowAppStore`, `allowWebStore`를 함께 받습니다.
201
209
 
210
+ `onAttempt`는 각 후보 시도 직전에 동기로 호출됩니다. 대기 UI를 여기서 그리세요 — 그러지
211
+ 않으면 타임아웃 동안 화면에 아무 변화가 없습니다.
212
+
213
+ ```js
214
+ await LaunchKit.app({
215
+ android: { scheme: 'myapp://profile/42', packageName: 'com.example.myapp', allowWebStore: true },
216
+ onAttempt({ by, index, total }) {
217
+ setStatus(by === 'store' ? '스토어로 이동합니다' : '앱을 여는 중… (' + (index + 1) + '/' + total + ')')
218
+ },
219
+ })
220
+ ```
221
+
222
+ 이미 진행 중인 호출이 있으면 **같은 프로미스를 반환**합니다. 연타해도 두 번째 체인이 첫 번째
223
+ 내비게이션을 덮어쓰는 일이 없습니다.
224
+
202
225
  안드로이드와 iOS는 `assumeAllowedInApp`을 추가로 받습니다. 파트너 화이트리스트로 동작하는
203
226
  인앱 브라우저(예: 웨이보)에 앱이 등록되어 있을 때만 `true`로 선언하세요. 그러면 그 환경에서
204
227
  scheme·유니버설 링크 후보가 유지됩니다. 완전히 차단된 웹뷰나 OS 버전 요구사항은 이 옵션으로
@@ -233,6 +256,9 @@ const files = await LaunchKit.filepicker({ accept: ['image/*', '.pdf'], multiple
233
256
  const tree = await LaunchKit.filepicker({ directory: true })
234
257
  ```
235
258
 
259
+ 취소하면 `[]`로 resolve합니다. `cancel` 이벤트가 있는 엔진은 즉시 알려주고, 나머지는 값이 빈
260
+ 상태로 포커스가 돌아오는 것으로 판별하므로 조금 더 걸립니다.
261
+
236
262
  ## 지도
237
263
 
238
264
  `map()`은 검색어·좌표·경로 중 하나를 받아 현재 OS에 맞는 URL을 만듭니다
@@ -259,6 +285,24 @@ await LaunchKit.map({ directions: { destination: 'Seoul Station', origin: [37.56
259
285
  경로는 Google Maps 폴백을 쓰고, Windows도 지도 앱이 단종되어 `bingmaps:`가 대개 웹 폴백으로
260
286
  넘어갑니다.
261
287
 
288
+ ## 콘솔에서 테스트하기
289
+
290
+ DevTools 콘솔에서 실행 메서드를 호출하면 페이지에 포커스도, 사용자 activation도 없는
291
+ 상태가 됩니다. 둘 다 프로그램으로는 되돌릴 수 없어서 시도가 실패하고 앱 전환 감지도 볼 것이
292
+ 없습니다. 같은 호출을 클릭 핸들러 안에서 하면 정상 동작합니다.
293
+
294
+ `debug`를 켜면 화면 이동 직전에 정지합니다. 재개 버튼을 누르면 포커스와 activation이
295
+ 돌아와서 콘솔에서도 실제 탭과 동일하게 동작합니다.
296
+
297
+ ```js
298
+ LaunchKit.debug = true
299
+
300
+ await LaunchKit.app({ ios: { scheme: 'myapp://profile/42' } })
301
+ ```
302
+
303
+ 그 외에는 꺼두세요. 모든 실행 메서드가 같은 지점을 지나므로 `telephone`, `message`,
304
+ `mail`, `map`에서도 후보마다 한 번씩 정지합니다.
305
+
262
306
  ## 시스템 설정
263
307
 
264
308
  ```js
@@ -279,15 +323,20 @@ if (LaunchKit.utils.canOpenSetting) {
279
323
  - **`app()`이 반환하는 것은 경로이지 실행 보증이 아닙니다.** 감지는 OS별 타임아웃을 둔
280
324
  포커스·가시성 휴리스틱에 기반합니다. `AppOpenedBy`가 반환됐다는 건 그 후보가 시도되었고
281
325
  페이지가 백그라운드로 전환된 것처럼 보였다는 뜻이지, 확정된 실행 확인은 아닙니다.
282
- - **`utils.getTrackId` / `getProductId`는 비동기**이고 미리 호출해도 안전합니다. 제스처 전에
283
- id를 확보해두면 `app()`이 조회를 인라인으로 하지 않습니다.
326
+ - **`utils.getTrackId` / `getProductId`는 비동기**이고 1시간 캐시됩니다. 페이지 초기화
327
+ 호출해두면 `app()`이 id를 인라인으로 조회하지 않습니다. `app()`은 사용자 제스처 안에서
328
+ 실행돼야 하므로, id가 없으면 `await`으로 user activation을 잃는 대신 동기로 조회합니다.
329
+ `trackId` / `productId`를 직접 주면 조회 자체가 생략됩니다.
284
330
  - **스토어 id 조회는 외부 API에 의존합니다**(iTunes Lookup, Microsoft display catalog).
285
331
  1시간 캐시되며 실패는 예외가 아니라 `undefined`로 resolve됩니다.
286
332
  - **인앱 브라우저는 후보 단위로 걸러집니다.** 실행이 차단된 웹뷰(WeChat, QQ, Qzone, Baidu,
287
333
  그리고 파트너가 아닌 웨이보) 안에서는 커스텀 스킴·`intent://`·`market://` 같은 스토어 스킴이
288
334
  타임아웃을 소진하지 않고 미리 제외됩니다. 그래서 체인이 곧바로 `fallback` / 웹스토어로
289
335
  떨어집니다. 화이트리스트에 등록된 파트너 앱은 `assumeAllowedInApp: true`로 되돌립니다.
290
- - **`map()`은 각 후보에 OS별 타임아웃**을 사용합니다.
336
+ - **`map()`은 각 후보에 OS별 타임아웃**을 사용하고, `app()`처럼 성공한 경로를 반환합니다.
337
+ - **웹 폴백은 내비게이션 시점에 resolve합니다.** `fallback`이나 웹스토어 후보는 문서 자체를
338
+ 교체하므로 기다릴 앱 전환이 없습니다. 이동을 시작하는 즉시 resolve하며, 이동이 성공한 뒤에
339
+ reject되는 일이 없습니다.
291
340
 
292
341
  ## 브라우저 지원
293
342
 
package/README.md CHANGED
@@ -25,18 +25,26 @@ The bundle is self-contained (OS/locale detection is inlined) — no peer script
25
25
  | Member | Signature | Description |
26
26
  | --- | --- | --- |
27
27
  | `LaunchKit.version` | `string` | The installed package version |
28
+ | `LaunchKit.debug` | `boolean` (get/set) | Pause on a `debugger` before each launch attempt; defaults to `false` |
28
29
  | `LaunchKit.SettingType` | `enum` | Setting panes: `General`, `Network`, `Display`, `Appearance`, `Accessibility`, `Battery`, `Datetime`, `Language`, `Accounts`, `Storage` |
29
30
  | `LaunchKit.app(options?)` | `Promise<AppOpenedBy>` | Launch an app via the best available route; resolves with which route opened it |
30
31
  | `LaunchKit.telephone(options?)` | `Promise<void>` | Open the dialer (`tel:`) |
31
32
  | `LaunchKit.message(options?)` | `Promise<void>` | Open the SMS composer (`sms:`) |
32
33
  | `LaunchKit.mail(options?)` | `Promise<void>` | Open the mail composer (`mailto:`) |
33
- | `LaunchKit.map(options?)` | `Promise<void>` | Open a map by query, coordinate, or directions; native app per-OS with a Google Maps web fallback |
34
+ | `LaunchKit.map(options?)` | `Promise<AppOpenedBy>` | Open a map by query, coordinate, or directions; native app per-OS with a Google Maps web fallback |
34
35
  | `LaunchKit.filepicker(options?)` | `Promise<File[]>` | Pick files or a directory (File System Access API, with input fallback) |
35
36
  | `LaunchKit.setting(type?)` | `Promise<void>` | Open a system-settings pane where supported |
36
37
  | `LaunchKit.utils` | object | `canOpenIntent` / `canOpenUniversal` / `canOpenSetting` getters, plus async `getTrackId` / `getProductId` |
37
38
 
38
39
  `AppOpenedBy` is one of: `"scheme"`, `"universal"`, `"intent"`, `"fallback"`, `"store"`.
39
40
 
41
+ `app()` and `map()` reject with a `LaunchError` — a real `Error` carrying two extra fields:
42
+
43
+ | Field | Type | Description |
44
+ | --- | --- | --- |
45
+ | `code` | `"unsupported-os" \| "no-candidates" \| "all-failed"` | Why it failed |
46
+ | `attempted` | `readonly LaunchAttempt[]` | `{ by, url, index, total }` for every candidate tried |
47
+
40
48
  ---
41
49
 
42
50
  ## ESM
@@ -201,6 +209,21 @@ Per-platform fields: Android accepts `intent` / `scheme` / `packageName` / `fall
201
209
  macOS accepts `scheme` / `bundleId` / `trackId`. All accept `fallback`, `timeout`,
202
210
  `allowAppStore`, `allowWebStore`.
203
211
 
212
+ `onAttempt` fires synchronously before each candidate, which is where you drive the waiting
213
+ UI — there is otherwise nothing on screen for the length of the timeout.
214
+
215
+ ```js
216
+ await LaunchKit.app({
217
+ android: { scheme: 'myapp://profile/42', packageName: 'com.example.myapp', allowWebStore: true },
218
+ onAttempt({ by, index, total }) {
219
+ setStatus(by === 'store' ? '스토어로 이동합니다' : '앱을 여는 중… (' + (index + 1) + '/' + total + ')')
220
+ },
221
+ })
222
+ ```
223
+
224
+ A second call while one is still running returns the same promise, so a double tap cannot
225
+ start a second chain that navigates over the first.
226
+
204
227
  Android and iOS additionally accept
205
228
  `assumeAllowedInApp` — declare it `true` only when your app is whitelisted by a
206
229
  partner-gated in-app browser (e.g. Weibo) so scheme / universal-link candidates are
@@ -235,6 +258,9 @@ const files = await LaunchKit.filepicker({ accept: ['image/*', '.pdf'], multiple
235
258
  const tree = await LaunchKit.filepicker({ directory: true })
236
259
  ```
237
260
 
261
+ Cancelling resolves with `[]`. Engines with a `cancel` event report it immediately; the rest
262
+ are detected by regaining focus with an empty value, which takes a moment longer.
263
+
238
264
  ## Maps
239
265
 
240
266
  `map()` takes an intent — a search query, a coordinate, or a route — and builds the
@@ -262,6 +288,24 @@ handlers other than Google Maps misread the labelled form as a search. Android's
262
288
  fallback there, and on Windows the discontinued Maps app means `bingmaps:` usually
263
289
  defers to the web fallback too.
264
290
 
291
+ ## Testing from the console
292
+
293
+ Calling a launch method from the DevTools console leaves the page without focus and without
294
+ user activation. Neither can be restored programmatically, so the attempt fails and
295
+ app-switch detection has nothing to observe — the same call from a click handler works fine.
296
+
297
+ Set `debug` to pause before the navigation. Pressing resume hands focus and activation back,
298
+ so the console behaves like a real tap.
299
+
300
+ ```js
301
+ LaunchKit.debug = true
302
+
303
+ await LaunchKit.app({ ios: { scheme: 'myapp://profile/42' } })
304
+ ```
305
+
306
+ Leave it off otherwise — every launch method routes through the same place, so it would
307
+ pause on `telephone`, `message`, `mail`, and `map` too, once per candidate.
308
+
265
309
  ## System settings
266
310
 
267
311
  ```js
@@ -282,8 +326,10 @@ if (LaunchKit.utils.canOpenSetting) {
282
326
  - **`app()` resolves with the route, not a guarantee of launch.** Detection relies on
283
327
  focus/visibility heuristics with per-OS timeouts; a resolved `AppOpenedBy` means that
284
328
  candidate was attempted and the page appeared to background, not a hard confirmation.
285
- - **`utils.getTrackId` / `getProductId` are async** and safe to call ahead of time
286
- resolving an id before the gesture keeps `app()` from looking it up inline.
329
+ - **`utils.getTrackId` / `getProductId` are async** and cached for an hour, so calling one
330
+ during page setup keeps `app()` from resolving the id inline. `app()` must run inside a
331
+ user gesture, so when the id is missing it looks it up synchronously rather than awaiting
332
+ and losing user activation. Passing `trackId` / `productId` directly skips it entirely.
287
333
  - **Store-id lookups depend on remote APIs** (iTunes Lookup, Microsoft display catalog)
288
334
  and are cached for one hour; failures resolve to `undefined` rather than throwing.
289
335
  - **In-app browsers are gated per candidate.** Inside webviews that block launches
@@ -292,7 +338,11 @@ if (LaunchKit.utils.canOpenSetting) {
292
338
  burning their timeouts, so the chain falls straight through to `fallback` / web store
293
339
  (the only routes that work there). Whitelisted partner apps opt back in with
294
340
  `assumeAllowedInApp: true`.
295
- - **`map()` uses the per-OS timeout** for each candidate.
341
+ - **`map()` uses the per-OS timeout** for each candidate, and resolves with the route that
342
+ worked just like `app()`.
343
+ - **Web fallbacks resolve on navigation.** A `fallback` or web-store candidate replaces the
344
+ document, so there is no app switch to wait for — those resolve as soon as the navigation
345
+ is issued instead of risking a rejection after the move already succeeded.
296
346
 
297
347
  ## Browser support
298
348
 
package/dist/index.d.ts CHANGED
@@ -3,6 +3,17 @@ declare type URLCandidateOrFallback = URLCandidate | (() => any);
3
3
  declare type URLStringOrFallback = string | (() => any);
4
4
  declare type OpenPickerStartIn = 'desktop' | 'documents' | 'downloads' | 'music' | 'pictures' | 'videos';
5
5
  declare type AppOpenedBy = 'scheme' | 'universal' | 'intent' | 'fallback' | 'store';
6
+ declare type LaunchErrorCode = 'unsupported-os' | 'no-candidates' | 'all-failed';
7
+ declare interface LaunchAttempt {
8
+ readonly by: AppOpenedBy;
9
+ readonly url: string;
10
+ readonly index: number;
11
+ readonly total: number;
12
+ }
13
+ declare interface LaunchError extends Error {
14
+ readonly code: LaunchErrorCode;
15
+ readonly attempted: readonly LaunchAttempt[];
16
+ }
6
17
  declare enum SettingType {
7
18
  General = "general",
8
19
  Network = "network",
@@ -46,6 +57,7 @@ declare interface AppOpenOptions {
46
57
  ios?: IOSAppInfo;
47
58
  windows?: WindowsAppInfo;
48
59
  macos?: MacOSAppInfo;
60
+ onAttempt?: (attempt: LaunchAttempt) => void;
49
61
  }
50
62
  declare interface TelephoneOptions {
51
63
  to?: string;
@@ -78,6 +90,7 @@ declare interface MapOptions {
78
90
  };
79
91
  zoom?: number;
80
92
  fallback?: URLStringOrFallback;
93
+ onAttempt?: (attempt: LaunchAttempt) => void;
81
94
  }
82
95
  interface LaunchKitUtils {
83
96
  get canOpenIntent(): boolean;
@@ -89,16 +102,18 @@ interface LaunchKitUtils {
89
102
  interface LaunchKitInstance {
90
103
  readonly version: string;
91
104
  readonly SettingType: typeof SettingType;
105
+ get debug(): boolean;
106
+ set debug(value: boolean);
92
107
  readonly utils: LaunchKitUtils;
93
108
  app(options?: AppOpenOptions): Promise<AppOpenedBy>;
94
109
  telephone(options?: TelephoneOptions): Promise<void>;
95
110
  message(options?: MessageOptions): Promise<void>;
96
111
  mail(options?: MailOptions): Promise<void>;
97
- map(options?: MapOptions): Promise<void>;
112
+ map(options?: MapOptions): Promise<AppOpenedBy>;
98
113
  filepicker(options?: FilepickerOptions): Promise<File[]>;
99
114
  setting(type?: SettingType): Promise<void>;
100
115
  }
101
116
  declare const LaunchKit: LaunchKitInstance;
102
117
 
103
118
  export { SettingType, LaunchKit as default };
104
- export type { AndroidAppInfo, AppInfo, AppOpenOptions, AppOpenedBy, FilepickerOptions, IOSAppInfo, LaunchKitInstance, MacOSAppInfo, MailOptions, MapOptions, MessageOptions, OpenPickerStartIn, TelephoneOptions, URLCandidate, URLCandidateOrFallback, URLStringOrFallback, WindowsAppInfo };
119
+ export type { AndroidAppInfo, AppInfo, AppOpenOptions, AppOpenedBy, FilepickerOptions, IOSAppInfo, LaunchAttempt, LaunchError, LaunchErrorCode, LaunchKitInstance, MacOSAppInfo, MailOptions, MapOptions, MessageOptions, OpenPickerStartIn, TelephoneOptions, WindowsAppInfo };
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var version$3 = "0.0.4";
5
+ var version$3 = "0.0.6";
6
6
  var packageJSON$3 = {
7
7
  version: version$3};
8
8
 
@@ -262,7 +262,7 @@ function parseWebview() {
262
262
  return true;
263
263
  if (/\belectron\//i.test(currentUserAgent))
264
264
  return true;
265
- if (/(?:iphone|ipad|ipod)/i.test(currentUserAgent) && /applewebkit/i.test(currentUserAgent) && /safari/i.test(currentUserAgent) === false)
265
+ if (/iphone|ipad|ipod/i.test(currentUserAgent) && /applewebkit/i.test(currentUserAgent) && /safari/i.test(currentUserAgent) === false)
266
266
  return true;
267
267
  return parseInAppBrowser() !== null;
268
268
  }
@@ -492,7 +492,7 @@ function getGlobal$2() {
492
492
  return {};
493
493
  }
494
494
 
495
- var version$1 = "0.0.1";
495
+ var version$1 = "0.0.3";
496
496
  var packageJSON$1 = {
497
497
  version: version$1};
498
498
 
@@ -942,8 +942,6 @@ var GLOBAL$4 = getGlobal$2();
942
942
  var LISTENER_STORE = [];
943
943
  var NOOP = function () {
944
944
  };
945
- // Records live on the target itself, so they are collected with it and lookups only scan
946
- // that target's own listeners. Targets that reject the expando fall back to LISTENER_STORE.
947
945
  var STORE_KEY = '__webEventKitListeners__';
948
946
  function resolveBucket(target, create) {
949
947
  var holder = target;
@@ -1001,9 +999,6 @@ function invokeCallback(callback, target, event) {
1001
999
  if (typeof callback === 'object' && callback !== null && typeof callback.handleEvent === 'function')
1002
1000
  return callback.handleEvent(event);
1003
1001
  }
1004
- // The same Event object is handed to every listener, so any property this kit overrides has
1005
- // to be put back before the next one runs — otherwise a `pointerdown` listener would observe
1006
- // the `touchstart` type and synthetic touches written by a listener registered before it.
1007
1002
  function patchEventProperty(event, property, value, patches) {
1008
1003
  var holder = event;
1009
1004
  var had = false;
@@ -1169,8 +1164,6 @@ var EventKit = {
1169
1164
  return NOOP;
1170
1165
  var normalized = normalizeListenerOptions(options);
1171
1166
  var existing = findListenerRecord(target, type, callback, normalized.capture);
1172
- // Native `addEventListener` ignores a duplicate registration, so hand back a releaser
1173
- // that does nothing rather than one that would detach the first caller's listener.
1174
1167
  if (typeof existing !== 'undefined')
1175
1168
  return NOOP;
1176
1169
  if (typeof normalized.signal !== 'undefined' && normalized.signal.aborted)
@@ -1200,8 +1193,6 @@ var EventKit = {
1200
1193
  record.onAbort = function () {
1201
1194
  releaseListenerRecord(record);
1202
1195
  };
1203
- // Engines without listener-options coerce `{once: true}` to capture, which would then
1204
- // fail to match on removal; releaseListenerRecord detaches this anyway.
1205
1196
  record.signal.addEventListener('abort', record.onAbort, false);
1206
1197
  }
1207
1198
  return function () {
@@ -1906,6 +1897,8 @@ var DEFAULT_TIMEOUT = 750;
1906
1897
  var GLOBAL = getGlobal$1();
1907
1898
  var NAVIGATOR = GLOBAL.navigator;
1908
1899
  var CLEANUP_INPUT_ELEMENT = null;
1900
+ var DEBUG = false;
1901
+ var APP_IN_FLIGHT = null;
1909
1902
  function createTypeGuard(tag) {
1910
1903
  return function (value) {
1911
1904
  return Object.prototype.toString.call(value) === '[object ' + tag + ']';
@@ -2122,7 +2115,8 @@ function resolveFocusEventConfig() {
2122
2115
  };
2123
2116
  }
2124
2117
  }
2125
- function openURL(tried, url, timeout) {
2118
+ function openURL(tried, url, timeout, terminal) {
2119
+ if (terminal === void 0) { terminal = false; }
2126
2120
  var config = resolveFocusEventConfig();
2127
2121
  var top = getTopmostWindow();
2128
2122
  var topDocument = top.document;
@@ -2160,6 +2154,8 @@ function openURL(tried, url, timeout) {
2160
2154
  }
2161
2155
  }
2162
2156
  }
2157
+ if (terminal)
2158
+ return;
2163
2159
  try {
2164
2160
  iframe = createHiddenElement('iframe');
2165
2161
  iframe.src = url;
@@ -2178,8 +2174,20 @@ function openURL(tried, url, timeout) {
2178
2174
  catch (_) {
2179
2175
  }
2180
2176
  }
2177
+ if (terminal) {
2178
+ return new Promise(function (resolve, reject) {
2179
+ try {
2180
+ open();
2181
+ resolve();
2182
+ }
2183
+ catch (_) {
2184
+ reject();
2185
+ }
2186
+ });
2187
+ }
2181
2188
  return new Promise(function (resolve, reject) {
2182
- debugger;
2189
+ if (DEBUG)
2190
+ debugger;
2183
2191
  var timeoutId;
2184
2192
  var resolved = false;
2185
2193
  function cleanup() {
@@ -2258,11 +2266,78 @@ function openURL(tried, url, timeout) {
2258
2266
  }
2259
2267
  });
2260
2268
  }
2269
+ var FUNCTION_FALLBACK_LABEL = '[function fallback]';
2270
+ var CANCEL_DELAY = 1000;
2271
+ function createLaunchError(code, message, attempted) {
2272
+ var error = new Error(message);
2273
+ var holder = error;
2274
+ holder['code'] = code;
2275
+ holder['attempted'] = attempted;
2276
+ return error;
2277
+ }
2278
+ function isWebURL(url) {
2279
+ return /^https?:\/\//i.test(url);
2280
+ }
2281
+ function isTerminalCandidate(by, url) {
2282
+ return (by === 'fallback' || by === 'store') && isWebURL(url);
2283
+ }
2284
+ function notifyAttempt(onAttempt, attempt) {
2285
+ if (typeof onAttempt !== 'function')
2286
+ return;
2287
+ try {
2288
+ onAttempt(attempt);
2289
+ }
2290
+ catch (_) {
2291
+ }
2292
+ }
2293
+ function describeAttempts(attempted) {
2294
+ var routes = [];
2295
+ for (var i = 0; i < attempted.length; i++)
2296
+ routes.push(attempted[i].by);
2297
+ return ' ' + attempted.length + ' candidate(s) attempted (' + joining(routes, undefined, ' \u2b62 ') + '). Read error.attempted for the URLs.';
2298
+ }
2299
+ function runCandidates(candidates, timeout, onAttempt, failure) {
2300
+ return new Promise(function (resolve, reject) {
2301
+ var attempted = [];
2302
+ var total = candidates.length;
2303
+ function step(index) {
2304
+ if (index >= total)
2305
+ return reject(createLaunchError('all-failed', failure + describeAttempts(attempted), attempted));
2306
+ var by = candidates[index][0];
2307
+ var url = candidates[index][1];
2308
+ var attempt = {
2309
+ by: by,
2310
+ url: typeof url === 'string' ? url : FUNCTION_FALLBACK_LABEL,
2311
+ index: index,
2312
+ total: total,
2313
+ };
2314
+ attempted.push(attempt);
2315
+ notifyAttempt(onAttempt, attempt);
2316
+ if (typeof url !== 'string') {
2317
+ try {
2318
+ url();
2319
+ }
2320
+ catch (_) {
2321
+ return step(index + 1);
2322
+ }
2323
+ return resolve(by);
2324
+ }
2325
+ openURL(index, url, timeout, isTerminalCandidate(by, url))
2326
+ .then(function () {
2327
+ resolve(by);
2328
+ })
2329
+ .catch(function () {
2330
+ step(index + 1);
2331
+ });
2332
+ }
2333
+ step(0);
2334
+ });
2335
+ }
2261
2336
  function resolveOptions(option) {
2262
2337
  var resolved = [];
2263
2338
  var os = PlatformKit.os.name;
2264
2339
  if (os === 'unknown')
2265
- throw new Error('Cannot resolve app open options: unsupported or undetected OS. (userAgent: "' + PlatformKit.userAgent + '")');
2340
+ throw createLaunchError('unsupported-os', 'Cannot resolve app open options: unsupported or undetected OS. (userAgent: "' + PlatformKit.userAgent + '")', []);
2266
2341
  var scheme;
2267
2342
  var fallback;
2268
2343
  var allowAppStore;
@@ -2692,7 +2767,7 @@ function resolveFile(module) {
2692
2767
  done(true);
2693
2768
  else
2694
2769
  done(false);
2695
- }, 1000);
2770
+ }, CANCEL_DELAY);
2696
2771
  }
2697
2772
  function onvisibilitychange() {
2698
2773
  if (!isDocumentHidden())
@@ -2803,6 +2878,12 @@ function walk(directory, basePath) {
2803
2878
  var LaunchKit = {
2804
2879
  SettingType: exports.SettingType,
2805
2880
  version: packageJSON$3.version,
2881
+ get debug() {
2882
+ return DEBUG;
2883
+ },
2884
+ set debug(value) {
2885
+ DEBUG = value === true;
2886
+ },
2806
2887
  utils: {
2807
2888
  get canOpenIntent() {
2808
2889
  return canOpenIntent();
@@ -2818,6 +2899,8 @@ var LaunchKit = {
2818
2899
  },
2819
2900
  app: function (options) {
2820
2901
  if (options === void 0) { options = {}; }
2902
+ if (APP_IN_FLIGHT !== null)
2903
+ return APP_IN_FLIGHT;
2821
2904
  var resolved;
2822
2905
  try {
2823
2906
  resolved = resolveOptions(options);
@@ -2828,39 +2911,15 @@ var LaunchKit = {
2828
2911
  var urls = resolved[0];
2829
2912
  var timeout = resolved[1];
2830
2913
  if (urls.length === 0)
2831
- return Promise.reject(new Error('No openable URL candidates were resolved for the current OS ("' + PlatformKit.os.name + '"). Provide at least one of: scheme, intent, universal, fallback, or store id for this platform.'));
2832
- return new Promise(function (resolve, reject) {
2833
- var tried = [];
2834
- function openURLSequential(index) {
2835
- if (index === void 0) { index = 0; }
2836
- if (index >= urls.length)
2837
- return reject(new Error('Failed to open the application using all available URLs.\n\n' + 'Attempted URLs:\n' + joining(tried, undefined, '\n↓\n')));
2838
- var entry = urls[index];
2839
- var by = entry[0];
2840
- var url = entry[1];
2841
- if (typeof url === 'string') {
2842
- tried[index] = url;
2843
- return openURL(index, url, timeout)
2844
- .then(function () {
2845
- resolve(by);
2846
- })
2847
- .catch(function () {
2848
- openURLSequential(index + 1);
2849
- });
2850
- }
2851
- else {
2852
- tried[index] = '[function fallback]';
2853
- try {
2854
- url();
2855
- }
2856
- catch (_) {
2857
- return openURLSequential(index + 1);
2858
- }
2859
- resolve(by);
2860
- }
2861
- }
2862
- return openURLSequential();
2863
- });
2914
+ return Promise.reject(createLaunchError('no-candidates', 'No openable URL candidates were resolved for the current OS ("' + PlatformKit.os.name + '"). Provide at least one of: scheme, intent, universal, fallback, or store id for this platform.', []));
2915
+ var attempt = runCandidates(urls, timeout, options.onAttempt, 'Failed to open the application.');
2916
+ function release() {
2917
+ if (APP_IN_FLIGHT === attempt)
2918
+ APP_IN_FLIGHT = null;
2919
+ }
2920
+ APP_IN_FLIGHT = attempt;
2921
+ attempt.then(release, release);
2922
+ return attempt;
2864
2923
  },
2865
2924
  telephone: function (options) {
2866
2925
  if (options === void 0) { options = {}; }
@@ -2910,7 +2969,7 @@ var LaunchKit = {
2910
2969
  map: function (options) {
2911
2970
  if (options === void 0) { options = {}; }
2912
2971
  var params = [];
2913
- var urls = [];
2972
+ var candidates = [];
2914
2973
  var url = '';
2915
2974
  switch (PlatformKit.os.name) {
2916
2975
  case 'android':
@@ -2928,7 +2987,7 @@ var LaunchKit = {
2928
2987
  params.push('z=' + options.zoom);
2929
2988
  if (params.length > 0)
2930
2989
  url = url + '?' + joining(params, undefined, '&');
2931
- urls.push(url);
2990
+ candidates.push(['scheme', url]);
2932
2991
  break;
2933
2992
  case 'ios':
2934
2993
  case 'macos':
@@ -2948,7 +3007,7 @@ var LaunchKit = {
2948
3007
  }
2949
3008
  if (typeof options.zoom !== 'undefined')
2950
3009
  params.push('z=' + options.zoom);
2951
- urls.push('maps://?' + joining(params, undefined, '&'));
3010
+ candidates.push(['scheme', 'maps://?' + joining(params, undefined, '&')]);
2952
3011
  break;
2953
3012
  case 'windows':
2954
3013
  if (typeof options.directions !== 'undefined') {
@@ -2964,10 +3023,10 @@ var LaunchKit = {
2964
3023
  }
2965
3024
  if (typeof options.zoom !== 'undefined')
2966
3025
  params.push('lvl=' + options.zoom);
2967
- urls.push('bingmaps:?' + joining(params, undefined, '&'));
3026
+ candidates.push(['scheme', 'bingmaps:?' + joining(params, undefined, '&')]);
2968
3027
  }
2969
3028
  if (typeof options.fallback !== 'undefined') {
2970
- urls.push(stripURL(options.fallback));
3029
+ candidates.push(['fallback', stripURL(options.fallback)]);
2971
3030
  }
2972
3031
  else {
2973
3032
  var params_1 = ['api=1'];
@@ -2975,7 +3034,7 @@ var LaunchKit = {
2975
3034
  params_1.push('destination=' + encodeMapValue(options.directions.destination));
2976
3035
  if (typeof options.directions.origin !== 'undefined')
2977
3036
  params_1.push('origin=' + encodeMapValue(options.directions.origin));
2978
- urls.push('https://www.google.com/maps/dir/?' + joining(params_1, undefined, '&'));
3037
+ candidates.push(['fallback', 'https://www.google.com/maps/dir/?' + joining(params_1, undefined, '&')]);
2979
3038
  }
2980
3039
  else {
2981
3040
  if (typeof options.coordinate !== 'undefined')
@@ -2984,34 +3043,10 @@ var LaunchKit = {
2984
3043
  params_1.push('query=' + escapeURIComponentString(options.query));
2985
3044
  if (typeof options.zoom !== 'undefined')
2986
3045
  params_1.push('zoom=' + options.zoom);
2987
- urls.push('https://www.google.com/maps/search/?' + joining(params_1, undefined, '&'));
3046
+ candidates.push(['fallback', 'https://www.google.com/maps/search/?' + joining(params_1, undefined, '&')]);
2988
3047
  }
2989
3048
  }
2990
- return new Promise(function (resolve, reject) {
2991
- var tried = [];
2992
- function openURLSequential(index) {
2993
- if (index === void 0) { index = 0; }
2994
- if (index >= urls.length)
2995
- return reject(new Error('Failed to open a map using all available URLs.\n\nAttempted URLs:\n' + joining(tried, undefined, '\n↓\n')));
2996
- var url = urls[index];
2997
- if (typeof url === 'string') {
2998
- tried[index] = url;
2999
- return openURL(index, url, getDefaultTimeout())
3000
- .then(function () {
3001
- resolve();
3002
- })
3003
- .catch(function () {
3004
- openURLSequential(index + 1);
3005
- });
3006
- }
3007
- else {
3008
- tried[index] = '[function fallback]';
3009
- url();
3010
- resolve();
3011
- }
3012
- }
3013
- return openURLSequential();
3014
- });
3049
+ return runCandidates(candidates, getDefaultTimeout(), options.onAttempt, 'Failed to open a map.');
3015
3050
  },
3016
3051
  filepicker: function (options) {
3017
3052
  if (options === void 0) { options = {}; }