web-launch-kit 0.0.5 → 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
@@ -30,13 +30,20 @@ npm install web-launch-kit
30
30
  | `LaunchKit.telephone(options?)` | `Promise<void>` | 전화 앱 열기 (`tel:`) |
31
31
  | `LaunchKit.message(options?)` | `Promise<void>` | 문자 작성 열기 (`sms:`) |
32
32
  | `LaunchKit.mail(options?)` | `Promise<void>` | 메일 작성 열기 (`mailto:`) |
33
- | `LaunchKit.map(options?)` | `Promise<void>` | 검색어·좌표·경로로 지도 열기. OS별 네이티브 앱과 Google Maps 웹 폴백 |
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
36
  | `LaunchKit.utils` | object | `canOpenIntent` / `canOpenUniversal` / `canOpenSetting` getter와 비동기 `getTrackId` / `getProductId` |
37
37
 
38
38
  `AppOpenedBy`는 `"scheme"`, `"universal"`, `"intent"`, `"fallback"`, `"store"` 중 하나입니다.
39
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
+
40
47
  ---
41
48
 
42
49
  ## ESM
@@ -200,6 +207,21 @@ console.log(openedBy) // "universal" | "scheme" | "intent" | "fallback" | "store
200
207
  `scheme` / `packageFamilyName` / `productId`, macOS는 `scheme` / `bundleId` / `trackId`를
201
208
  받습니다. 모두 `fallback`, `timeout`, `allowAppStore`, `allowWebStore`를 함께 받습니다.
202
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
+
203
225
  안드로이드와 iOS는 `assumeAllowedInApp`을 추가로 받습니다. 파트너 화이트리스트로 동작하는
204
226
  인앱 브라우저(예: 웨이보)에 앱이 등록되어 있을 때만 `true`로 선언하세요. 그러면 그 환경에서
205
227
  scheme·유니버설 링크 후보가 유지됩니다. 완전히 차단된 웹뷰나 OS 버전 요구사항은 이 옵션으로
@@ -234,6 +256,9 @@ const files = await LaunchKit.filepicker({ accept: ['image/*', '.pdf'], multiple
234
256
  const tree = await LaunchKit.filepicker({ directory: true })
235
257
  ```
236
258
 
259
+ 취소하면 `[]`로 resolve합니다. `cancel` 이벤트가 있는 엔진은 즉시 알려주고, 나머지는 값이 빈
260
+ 상태로 포커스가 돌아오는 것으로 판별하므로 조금 더 걸립니다.
261
+
237
262
  ## 지도
238
263
 
239
264
  `map()`은 검색어·좌표·경로 중 하나를 받아 현재 OS에 맞는 URL을 만듭니다
@@ -298,15 +323,20 @@ if (LaunchKit.utils.canOpenSetting) {
298
323
  - **`app()`이 반환하는 것은 경로이지 실행 보증이 아닙니다.** 감지는 OS별 타임아웃을 둔
299
324
  포커스·가시성 휴리스틱에 기반합니다. `AppOpenedBy`가 반환됐다는 건 그 후보가 시도되었고
300
325
  페이지가 백그라운드로 전환된 것처럼 보였다는 뜻이지, 확정된 실행 확인은 아닙니다.
301
- - **`utils.getTrackId` / `getProductId`는 비동기**이고 미리 호출해도 안전합니다. 제스처 전에
302
- id를 확보해두면 `app()`이 조회를 인라인으로 하지 않습니다.
326
+ - **`utils.getTrackId` / `getProductId`는 비동기**이고 1시간 캐시됩니다. 페이지 초기화
327
+ 호출해두면 `app()`이 id를 인라인으로 조회하지 않습니다. `app()`은 사용자 제스처 안에서
328
+ 실행돼야 하므로, id가 없으면 `await`으로 user activation을 잃는 대신 동기로 조회합니다.
329
+ `trackId` / `productId`를 직접 주면 조회 자체가 생략됩니다.
303
330
  - **스토어 id 조회는 외부 API에 의존합니다**(iTunes Lookup, Microsoft display catalog).
304
331
  1시간 캐시되며 실패는 예외가 아니라 `undefined`로 resolve됩니다.
305
332
  - **인앱 브라우저는 후보 단위로 걸러집니다.** 실행이 차단된 웹뷰(WeChat, QQ, Qzone, Baidu,
306
333
  그리고 파트너가 아닌 웨이보) 안에서는 커스텀 스킴·`intent://`·`market://` 같은 스토어 스킴이
307
334
  타임아웃을 소진하지 않고 미리 제외됩니다. 그래서 체인이 곧바로 `fallback` / 웹스토어로
308
335
  떨어집니다. 화이트리스트에 등록된 파트너 앱은 `assumeAllowedInApp: true`로 되돌립니다.
309
- - **`map()`은 각 후보에 OS별 타임아웃**을 사용합니다.
336
+ - **`map()`은 각 후보에 OS별 타임아웃**을 사용하고, `app()`처럼 성공한 경로를 반환합니다.
337
+ - **웹 폴백은 내비게이션 시점에 resolve합니다.** `fallback`이나 웹스토어 후보는 문서 자체를
338
+ 교체하므로 기다릴 앱 전환이 없습니다. 이동을 시작하는 즉시 resolve하며, 이동이 성공한 뒤에
339
+ reject되는 일이 없습니다.
310
340
 
311
341
  ## 브라우저 지원
312
342
 
package/README.md CHANGED
@@ -31,13 +31,20 @@ The bundle is self-contained (OS/locale detection is inlined) — no peer script
31
31
  | `LaunchKit.telephone(options?)` | `Promise<void>` | Open the dialer (`tel:`) |
32
32
  | `LaunchKit.message(options?)` | `Promise<void>` | Open the SMS composer (`sms:`) |
33
33
  | `LaunchKit.mail(options?)` | `Promise<void>` | Open the mail composer (`mailto:`) |
34
- | `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 |
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
37
  | `LaunchKit.utils` | object | `canOpenIntent` / `canOpenUniversal` / `canOpenSetting` getters, plus async `getTrackId` / `getProductId` |
38
38
 
39
39
  `AppOpenedBy` is one of: `"scheme"`, `"universal"`, `"intent"`, `"fallback"`, `"store"`.
40
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
+
41
48
  ---
42
49
 
43
50
  ## ESM
@@ -202,6 +209,21 @@ Per-platform fields: Android accepts `intent` / `scheme` / `packageName` / `fall
202
209
  macOS accepts `scheme` / `bundleId` / `trackId`. All accept `fallback`, `timeout`,
203
210
  `allowAppStore`, `allowWebStore`.
204
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
+
205
227
  Android and iOS additionally accept
206
228
  `assumeAllowedInApp` — declare it `true` only when your app is whitelisted by a
207
229
  partner-gated in-app browser (e.g. Weibo) so scheme / universal-link candidates are
@@ -236,6 +258,9 @@ const files = await LaunchKit.filepicker({ accept: ['image/*', '.pdf'], multiple
236
258
  const tree = await LaunchKit.filepicker({ directory: true })
237
259
  ```
238
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
+
239
264
  ## Maps
240
265
 
241
266
  `map()` takes an intent — a search query, a coordinate, or a route — and builds the
@@ -301,8 +326,10 @@ if (LaunchKit.utils.canOpenSetting) {
301
326
  - **`app()` resolves with the route, not a guarantee of launch.** Detection relies on
302
327
  focus/visibility heuristics with per-OS timeouts; a resolved `AppOpenedBy` means that
303
328
  candidate was attempted and the page appeared to background, not a hard confirmation.
304
- - **`utils.getTrackId` / `getProductId` are async** and safe to call ahead of time
305
- 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.
306
333
  - **Store-id lookups depend on remote APIs** (iTunes Lookup, Microsoft display catalog)
307
334
  and are cached for one hour; failures resolve to `undefined` rather than throwing.
308
335
  - **In-app browsers are gated per candidate.** Inside webviews that block launches
@@ -311,7 +338,11 @@ if (LaunchKit.utils.canOpenSetting) {
311
338
  burning their timeouts, so the chain falls straight through to `fallback` / web store
312
339
  (the only routes that work there). Whitelisted partner apps opt back in with
313
340
  `assumeAllowedInApp: true`.
314
- - **`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.
315
346
 
316
347
  ## Browser support
317
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;
@@ -96,11 +109,11 @@ interface LaunchKitInstance {
96
109
  telephone(options?: TelephoneOptions): Promise<void>;
97
110
  message(options?: MessageOptions): Promise<void>;
98
111
  mail(options?: MailOptions): Promise<void>;
99
- map(options?: MapOptions): Promise<void>;
112
+ map(options?: MapOptions): Promise<AppOpenedBy>;
100
113
  filepicker(options?: FilepickerOptions): Promise<File[]>;
101
114
  setting(type?: SettingType): Promise<void>;
102
115
  }
103
116
  declare const LaunchKit: LaunchKitInstance;
104
117
 
105
118
  export { SettingType, LaunchKit as default };
106
- 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.5";
5
+ var version$3 = "0.0.6";
6
6
  var packageJSON$3 = {
7
7
  version: version$3};
8
8
 
@@ -1898,6 +1898,7 @@ var GLOBAL = getGlobal$1();
1898
1898
  var NAVIGATOR = GLOBAL.navigator;
1899
1899
  var CLEANUP_INPUT_ELEMENT = null;
1900
1900
  var DEBUG = false;
1901
+ var APP_IN_FLIGHT = null;
1901
1902
  function createTypeGuard(tag) {
1902
1903
  return function (value) {
1903
1904
  return Object.prototype.toString.call(value) === '[object ' + tag + ']';
@@ -2114,7 +2115,8 @@ function resolveFocusEventConfig() {
2114
2115
  };
2115
2116
  }
2116
2117
  }
2117
- function openURL(tried, url, timeout) {
2118
+ function openURL(tried, url, timeout, terminal) {
2119
+ if (terminal === void 0) { terminal = false; }
2118
2120
  var config = resolveFocusEventConfig();
2119
2121
  var top = getTopmostWindow();
2120
2122
  var topDocument = top.document;
@@ -2152,6 +2154,8 @@ function openURL(tried, url, timeout) {
2152
2154
  }
2153
2155
  }
2154
2156
  }
2157
+ if (terminal)
2158
+ return;
2155
2159
  try {
2156
2160
  iframe = createHiddenElement('iframe');
2157
2161
  iframe.src = url;
@@ -2170,6 +2174,17 @@ function openURL(tried, url, timeout) {
2170
2174
  catch (_) {
2171
2175
  }
2172
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
+ }
2173
2188
  return new Promise(function (resolve, reject) {
2174
2189
  if (DEBUG)
2175
2190
  debugger;
@@ -2251,11 +2266,78 @@ function openURL(tried, url, timeout) {
2251
2266
  }
2252
2267
  });
2253
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
+ }
2254
2336
  function resolveOptions(option) {
2255
2337
  var resolved = [];
2256
2338
  var os = PlatformKit.os.name;
2257
2339
  if (os === 'unknown')
2258
- 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 + '")', []);
2259
2341
  var scheme;
2260
2342
  var fallback;
2261
2343
  var allowAppStore;
@@ -2685,7 +2767,7 @@ function resolveFile(module) {
2685
2767
  done(true);
2686
2768
  else
2687
2769
  done(false);
2688
- }, 1000);
2770
+ }, CANCEL_DELAY);
2689
2771
  }
2690
2772
  function onvisibilitychange() {
2691
2773
  if (!isDocumentHidden())
@@ -2817,6 +2899,8 @@ var LaunchKit = {
2817
2899
  },
2818
2900
  app: function (options) {
2819
2901
  if (options === void 0) { options = {}; }
2902
+ if (APP_IN_FLIGHT !== null)
2903
+ return APP_IN_FLIGHT;
2820
2904
  var resolved;
2821
2905
  try {
2822
2906
  resolved = resolveOptions(options);
@@ -2827,39 +2911,15 @@ var LaunchKit = {
2827
2911
  var urls = resolved[0];
2828
2912
  var timeout = resolved[1];
2829
2913
  if (urls.length === 0)
2830
- 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.'));
2831
- return new Promise(function (resolve, reject) {
2832
- var tried = [];
2833
- function openURLSequential(index) {
2834
- if (index === void 0) { index = 0; }
2835
- if (index >= urls.length)
2836
- return reject(new Error('Failed to open the application using all available URLs.\n\n' + 'Attempted URLs:\n' + joining(tried, undefined, '\n↓\n')));
2837
- var entry = urls[index];
2838
- var by = entry[0];
2839
- var url = entry[1];
2840
- if (typeof url === 'string') {
2841
- tried[index] = url;
2842
- return openURL(index, url, timeout)
2843
- .then(function () {
2844
- resolve(by);
2845
- })
2846
- .catch(function () {
2847
- openURLSequential(index + 1);
2848
- });
2849
- }
2850
- else {
2851
- tried[index] = '[function fallback]';
2852
- try {
2853
- url();
2854
- }
2855
- catch (_) {
2856
- return openURLSequential(index + 1);
2857
- }
2858
- resolve(by);
2859
- }
2860
- }
2861
- return openURLSequential();
2862
- });
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;
2863
2923
  },
2864
2924
  telephone: function (options) {
2865
2925
  if (options === void 0) { options = {}; }
@@ -2909,7 +2969,7 @@ var LaunchKit = {
2909
2969
  map: function (options) {
2910
2970
  if (options === void 0) { options = {}; }
2911
2971
  var params = [];
2912
- var urls = [];
2972
+ var candidates = [];
2913
2973
  var url = '';
2914
2974
  switch (PlatformKit.os.name) {
2915
2975
  case 'android':
@@ -2927,7 +2987,7 @@ var LaunchKit = {
2927
2987
  params.push('z=' + options.zoom);
2928
2988
  if (params.length > 0)
2929
2989
  url = url + '?' + joining(params, undefined, '&');
2930
- urls.push(url);
2990
+ candidates.push(['scheme', url]);
2931
2991
  break;
2932
2992
  case 'ios':
2933
2993
  case 'macos':
@@ -2947,7 +3007,7 @@ var LaunchKit = {
2947
3007
  }
2948
3008
  if (typeof options.zoom !== 'undefined')
2949
3009
  params.push('z=' + options.zoom);
2950
- urls.push('maps://?' + joining(params, undefined, '&'));
3010
+ candidates.push(['scheme', 'maps://?' + joining(params, undefined, '&')]);
2951
3011
  break;
2952
3012
  case 'windows':
2953
3013
  if (typeof options.directions !== 'undefined') {
@@ -2963,10 +3023,10 @@ var LaunchKit = {
2963
3023
  }
2964
3024
  if (typeof options.zoom !== 'undefined')
2965
3025
  params.push('lvl=' + options.zoom);
2966
- urls.push('bingmaps:?' + joining(params, undefined, '&'));
3026
+ candidates.push(['scheme', 'bingmaps:?' + joining(params, undefined, '&')]);
2967
3027
  }
2968
3028
  if (typeof options.fallback !== 'undefined') {
2969
- urls.push(stripURL(options.fallback));
3029
+ candidates.push(['fallback', stripURL(options.fallback)]);
2970
3030
  }
2971
3031
  else {
2972
3032
  var params_1 = ['api=1'];
@@ -2974,7 +3034,7 @@ var LaunchKit = {
2974
3034
  params_1.push('destination=' + encodeMapValue(options.directions.destination));
2975
3035
  if (typeof options.directions.origin !== 'undefined')
2976
3036
  params_1.push('origin=' + encodeMapValue(options.directions.origin));
2977
- 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, '&')]);
2978
3038
  }
2979
3039
  else {
2980
3040
  if (typeof options.coordinate !== 'undefined')
@@ -2983,34 +3043,10 @@ var LaunchKit = {
2983
3043
  params_1.push('query=' + escapeURIComponentString(options.query));
2984
3044
  if (typeof options.zoom !== 'undefined')
2985
3045
  params_1.push('zoom=' + options.zoom);
2986
- 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, '&')]);
2987
3047
  }
2988
3048
  }
2989
- return new Promise(function (resolve, reject) {
2990
- var tried = [];
2991
- function openURLSequential(index) {
2992
- if (index === void 0) { index = 0; }
2993
- if (index >= urls.length)
2994
- return reject(new Error('Failed to open a map using all available URLs.\n\nAttempted URLs:\n' + joining(tried, undefined, '\n↓\n')));
2995
- var url = urls[index];
2996
- if (typeof url === 'string') {
2997
- tried[index] = url;
2998
- return openURL(index, url, getDefaultTimeout())
2999
- .then(function () {
3000
- resolve();
3001
- })
3002
- .catch(function () {
3003
- openURLSequential(index + 1);
3004
- });
3005
- }
3006
- else {
3007
- tried[index] = '[function fallback]';
3008
- url();
3009
- resolve();
3010
- }
3011
- }
3012
- return openURLSequential();
3013
- });
3049
+ return runCandidates(candidates, getDefaultTimeout(), options.onAttempt, 'Failed to open a map.');
3014
3050
  },
3015
3051
  filepicker: function (options) {
3016
3052
  if (options === void 0) { options = {}; }