@goodandready/dsh-plugin-notify 0.3.8 → 0.4.1
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/CHANGELOG.md +30 -5
- package/README.md +58 -4
- package/README.ru.md +48 -4
- package/README.zh.md +48 -4
- package/lib/client.js +240 -8
- package/lib/index.js +520 -39
- package/package.json +9 -3
package/lib/client.js
CHANGED
|
@@ -13,7 +13,7 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
|
|
14
14
|
const en = {
|
|
15
15
|
title: 'Notify',
|
|
16
|
-
subtitle: 'IM webhook, sound, toast, and
|
|
16
|
+
subtitle: 'IM webhook, sound, toast, desktop, and web push notifications',
|
|
17
17
|
intro: 'Store each webhook URL under Settings → Credentials, then type only the credential name below. Do not paste URLs into this form.',
|
|
18
18
|
'field.feishu': 'Feishu credential',
|
|
19
19
|
'field.wecom': 'WeCom credential',
|
|
@@ -51,10 +51,21 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
'settings.saving': 'Saving…',
|
|
52
52
|
'settings.saved': 'Saved',
|
|
53
53
|
'settings.saveFailed': 'Save failed: ',
|
|
54
|
+
'webpush.section': 'Web push (reaches devices with every dsh tab closed)',
|
|
55
|
+
'field.enableWebPush': 'Enable web push channel',
|
|
56
|
+
'field.webPushOnlyWhenAway': 'Only push when no browser is connected',
|
|
57
|
+
'btn.webpushEnable': 'Enable push on this device',
|
|
58
|
+
'btn.testPush': 'Test push',
|
|
59
|
+
'webpush.state.unsupported': 'Web push is not available in this context — open dsh in a browser (HTTPS or localhost) to use it',
|
|
60
|
+
'webpush.state.denied': 'Notification permission denied by browser settings',
|
|
61
|
+
'webpush.state.disabled': 'Web push channel is disabled — enable it above and save first',
|
|
62
|
+
'webpush.state.notSubscribed': 'This device is not subscribed yet',
|
|
63
|
+
'webpush.state.subscribed': 'This device is subscribed — push will reach it',
|
|
64
|
+
'webpush.state.error': 'Push setup failed: ',
|
|
54
65
|
}
|
|
55
66
|
const zh = {
|
|
56
67
|
title: '通知',
|
|
57
|
-
subtitle: 'IM Webhook
|
|
68
|
+
subtitle: 'IM Webhook、音频、浮动弹窗、桌面系统通知与网页推送',
|
|
58
69
|
intro: '请先在设置 → 凭据中保存 Webhook URL,然后只在下方填写凭据名称。不要直接粘贴 URL。',
|
|
59
70
|
'field.feishu': '飞书凭据',
|
|
60
71
|
'field.wecom': '企业微信凭据',
|
|
@@ -92,6 +103,17 @@ window.__ModuleLoader__.load({
|
|
|
92
103
|
'settings.saving': '正在保存…',
|
|
93
104
|
'settings.saved': '已保存',
|
|
94
105
|
'settings.saveFailed': '保存失败:',
|
|
106
|
+
'webpush.section': '网页推送(所有 dsh 标签页关闭时也能收到)',
|
|
107
|
+
'field.enableWebPush': '启用网页推送通道',
|
|
108
|
+
'field.webPushOnlyWhenAway': '仅在无浏览器连接时推送',
|
|
109
|
+
'btn.webpushEnable': '在本设备上启用推送',
|
|
110
|
+
'btn.testPush': '测试推送',
|
|
111
|
+
'webpush.state.unsupported': '当前上下文不支持网页推送——请在浏览器中打开 dsh(HTTPS 或 localhost)使用该通道',
|
|
112
|
+
'webpush.state.denied': '通知权限已被浏览器设置拒绝',
|
|
113
|
+
'webpush.state.disabled': '网页推送通道未启用——请先在上方启用并保存',
|
|
114
|
+
'webpush.state.notSubscribed': '此设备尚未订阅推送',
|
|
115
|
+
'webpush.state.subscribed': '此设备已订阅——可以接收推送',
|
|
116
|
+
'webpush.state.error': '推送设置失败:',
|
|
95
117
|
}
|
|
96
118
|
|
|
97
119
|
let ChevronIcon = null
|
|
@@ -250,6 +272,117 @@ window.__ModuleLoader__.load({
|
|
|
250
272
|
}
|
|
251
273
|
}
|
|
252
274
|
|
|
275
|
+
function urlBase64ToUint8Array(base64String) {
|
|
276
|
+
try {
|
|
277
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
|
278
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
|
279
|
+
const raw = window.atob(base64)
|
|
280
|
+
const output = new Uint8Array(raw.length)
|
|
281
|
+
for (let i = 0; i < raw.length; i++) output[i] = raw.charCodeAt(i)
|
|
282
|
+
return output
|
|
283
|
+
} catch (_err) {
|
|
284
|
+
return new Uint8Array(0)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Document-relative on purpose: the web document declares <base href="./">,
|
|
289
|
+
// so plugin routes resolve correctly behind a --public-url mount prefix.
|
|
290
|
+
function pluginUrl(relPath) {
|
|
291
|
+
try {
|
|
292
|
+
return new URL(relPath, document.baseURI).href
|
|
293
|
+
} catch (_err) {
|
|
294
|
+
return relPath
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function webPushSupported() {
|
|
299
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined') return false
|
|
300
|
+
if (!('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) return false
|
|
301
|
+
if (window.isSecureContext === false) return false
|
|
302
|
+
// Service workers only register on http(s) origins; desktop shells load
|
|
303
|
+
// the UI from custom schemes (e.g. dsh-app://) where they are unavailable.
|
|
304
|
+
const proto = window.location ? window.location.protocol : ''
|
|
305
|
+
if (proto !== 'https:' && proto !== 'http:') return false
|
|
306
|
+
return true
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function registerNotifyServiceWorker() {
|
|
310
|
+
if (!webPushSupported()) return Promise.reject(new Error('unsupported'))
|
|
311
|
+
return navigator.serviceWorker.register(pluginUrl('dsh-plugin-notify/sw.js'), { scope: './' })
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Permission → service worker → applicationServerKey → subscribe → upload.
|
|
315
|
+
async function enableWebPushOnDevice() {
|
|
316
|
+
if (!webPushSupported()) throw new Error('unsupported')
|
|
317
|
+
const perm = await Notification.requestPermission()
|
|
318
|
+
if (perm !== 'granted') throw new Error('denied')
|
|
319
|
+
const reg = await registerNotifyServiceWorker()
|
|
320
|
+
const keyRes = await fetch(pluginUrl('dsh-plugin-notify/push/key'))
|
|
321
|
+
if (!keyRes.ok) throw new Error('disabled')
|
|
322
|
+
const keyInfo = await keyRes.json().catch(() => null)
|
|
323
|
+
if (!keyInfo || !keyInfo.publicKey) throw new Error('disabled')
|
|
324
|
+
// A stale subscription bound to a previous key blocks re-subscription.
|
|
325
|
+
const existing = await reg.pushManager.getSubscription()
|
|
326
|
+
if (existing) await existing.unsubscribe()
|
|
327
|
+
const sub = await reg.pushManager.subscribe({
|
|
328
|
+
userVisibleOnly: true,
|
|
329
|
+
applicationServerKey: urlBase64ToUint8Array(keyInfo.publicKey),
|
|
330
|
+
})
|
|
331
|
+
const saveRes = await fetch(pluginUrl('dsh-plugin-notify/push/subscribe'), {
|
|
332
|
+
method: 'POST',
|
|
333
|
+
headers: { 'content-type': 'application/json' },
|
|
334
|
+
body: JSON.stringify(sub.toJSON ? sub.toJSON() : sub),
|
|
335
|
+
})
|
|
336
|
+
if (!saveRes.ok) throw new Error('subscribe failed (' + saveRes.status + ')')
|
|
337
|
+
return sub
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function refreshWebPushStatus() {
|
|
341
|
+
if (!webPushSupported()) return 'unsupported'
|
|
342
|
+
if (Notification.permission === 'denied') return 'denied'
|
|
343
|
+
try {
|
|
344
|
+
const keyRes = await fetch(pluginUrl('dsh-plugin-notify/push/key'))
|
|
345
|
+
if (!keyRes.ok) return 'disabled'
|
|
346
|
+
const reg = await navigator.serviceWorker.getRegistration()
|
|
347
|
+
if (!reg) return 'notSubscribed'
|
|
348
|
+
const sub = await reg.pushManager.getSubscription()
|
|
349
|
+
return sub ? 'subscribed' : 'notSubscribed'
|
|
350
|
+
} catch (_err) {
|
|
351
|
+
return 'notSubscribed'
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// A notification click that found no live window opened a fresh one with
|
|
356
|
+
// ?dshNotifySession=<id>: navigate once the sessions service shows up.
|
|
357
|
+
function consumeDeepLink(ctx) {
|
|
358
|
+
if (typeof window === 'undefined') return
|
|
359
|
+
let sessionId = ''
|
|
360
|
+
try {
|
|
361
|
+
const params = new URLSearchParams(window.location.search)
|
|
362
|
+
sessionId = params.get('dshNotifySession') || ''
|
|
363
|
+
if (!sessionId) return
|
|
364
|
+
params.delete('dshNotifySession')
|
|
365
|
+
const qs = params.toString()
|
|
366
|
+
window.history.replaceState({}, '', window.location.pathname + (qs ? '?' + qs : '') + window.location.hash)
|
|
367
|
+
} catch (_err) {
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
let tries = 0
|
|
371
|
+
const timer = setInterval(() => {
|
|
372
|
+
tries++
|
|
373
|
+
try {
|
|
374
|
+
if (ctx && ctx.sessions && typeof ctx.sessions.activate === 'function') {
|
|
375
|
+
ctx.sessions.activate(sessionId)
|
|
376
|
+
clearInterval(timer)
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
} catch (_err) {
|
|
380
|
+
// retry until the budget is exhausted
|
|
381
|
+
}
|
|
382
|
+
if (tries > 40) clearInterval(timer)
|
|
383
|
+
}, 250)
|
|
384
|
+
}
|
|
385
|
+
|
|
253
386
|
function getToastContainer() {
|
|
254
387
|
if (typeof document === 'undefined') return null
|
|
255
388
|
let container = document.getElementById('dsh-notify-toast-container')
|
|
@@ -487,6 +620,49 @@ window.__ModuleLoader__.load({
|
|
|
487
620
|
}
|
|
488
621
|
return 'unsupported'
|
|
489
622
|
})
|
|
623
|
+
const [webPushState, setWebPushState] = React.useState('')
|
|
624
|
+
const [webPushErr, setWebPushErr] = React.useState('')
|
|
625
|
+
|
|
626
|
+
React.useEffect(() => {
|
|
627
|
+
refreshWebPushStatus().then(setWebPushState).catch(() => {})
|
|
628
|
+
}, [])
|
|
629
|
+
|
|
630
|
+
const onEnableWebPush = async () => {
|
|
631
|
+
setWebPushErr('')
|
|
632
|
+
try {
|
|
633
|
+
await enableWebPushOnDevice()
|
|
634
|
+
setWebPushState('subscribed')
|
|
635
|
+
if (draft) setDraft({ ...draft, enableWebPush: true })
|
|
636
|
+
} catch (e) {
|
|
637
|
+
const msg = String((e && e.message) || e)
|
|
638
|
+
if (msg === 'denied' || msg === 'unsupported' || /not supported/i.test(msg)) {
|
|
639
|
+
setWebPushState('unsupported')
|
|
640
|
+
} else {
|
|
641
|
+
setWebPushState('error')
|
|
642
|
+
setWebPushErr(msg)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const onTestPush = async () => {
|
|
648
|
+
setWebPushErr('')
|
|
649
|
+
try {
|
|
650
|
+
const res = await fetch(pluginUrl('dsh-plugin-notify/push/test'), { method: 'POST' })
|
|
651
|
+
if (!res.ok) {
|
|
652
|
+
setWebPushState(res.status === 404 ? 'disabled' : 'error')
|
|
653
|
+
if (res.status !== 404) setWebPushErr('HTTP ' + res.status)
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
setWebPushState('subscribed')
|
|
657
|
+
} catch (e) {
|
|
658
|
+
setWebPushState('error')
|
|
659
|
+
setWebPushErr(String((e && e.message) || e))
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const webPushStatusText = webPushState === 'error'
|
|
664
|
+
? t('webpush.state.error') + webPushErr
|
|
665
|
+
: (webPushState ? t('webpush.state.' + webPushState) : '')
|
|
490
666
|
|
|
491
667
|
const requestPermission = async () => {
|
|
492
668
|
if (typeof window !== 'undefined' && 'Notification' in window) {
|
|
@@ -499,15 +675,22 @@ window.__ModuleLoader__.load({
|
|
|
499
675
|
}
|
|
500
676
|
}
|
|
501
677
|
|
|
502
|
-
|
|
503
|
-
|
|
678
|
+
// In DSH 0.2.x configForms is keyed strictly by entry id (ROW_ID = 'plugin-notify').
|
|
679
|
+
// Hold both candidates and prefer ROW_ID whenever it is ready or NS is unavailable.
|
|
680
|
+
const [scopeRow, scopeNS] = React.useMemo(
|
|
681
|
+
() => (ctx && ctx.configForms ? [ctx.configForms.get(ROW_ID), ctx.configForms.get(NS)] : [undefined, undefined]),
|
|
504
682
|
[ctx],
|
|
505
683
|
)
|
|
506
|
-
const
|
|
684
|
+
const useSnap = (scope) => React.useSyncExternalStore(
|
|
507
685
|
React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
|
|
508
686
|
React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]),
|
|
509
687
|
React.useCallback(() => ({ status: 'loading' }), []),
|
|
510
688
|
)
|
|
689
|
+
const snapRow = useSnap(scopeRow)
|
|
690
|
+
const snapNS = useSnap(scopeNS)
|
|
691
|
+
const useRow = scopeRow && (snapRow.status === 'ready' || snapNS.status !== 'ready')
|
|
692
|
+
const scope = useRow ? scopeRow : (scopeNS || scopeRow)
|
|
693
|
+
const snapshot = useRow ? snapRow : (scopeNS ? snapNS : snapRow)
|
|
511
694
|
|
|
512
695
|
React.useEffect(() => { ensureStyles() }, [])
|
|
513
696
|
|
|
@@ -529,6 +712,8 @@ window.__ModuleLoader__.load({
|
|
|
529
712
|
enableSound: Boolean(stored.enableSound),
|
|
530
713
|
enableToasts: Boolean(stored.enableToasts),
|
|
531
714
|
enableDesktopNotifications: Boolean(stored.enableDesktopNotifications),
|
|
715
|
+
enableWebPush: Boolean(stored.webPush && stored.webPush.enabled),
|
|
716
|
+
webPushOnlyWhenAway: !(stored.webPush && stored.webPush.onlyWhenAway === false),
|
|
532
717
|
notifyBackgroundOnly: !!stored.notifyBackgroundOnly,
|
|
533
718
|
timeoutMs: String(stored.timeoutMs != null ? stored.timeoutMs : 5000),
|
|
534
719
|
dndStart: (stored.dnd && stored.dnd.start) || '',
|
|
@@ -554,6 +739,7 @@ window.__ModuleLoader__.load({
|
|
|
554
739
|
enableSound: !!draft.enableSound,
|
|
555
740
|
enableToasts: !!draft.enableToasts,
|
|
556
741
|
enableDesktopNotifications: !!draft.enableDesktopNotifications,
|
|
742
|
+
webPush: { enabled: !!draft.enableWebPush, onlyWhenAway: draft.webPushOnlyWhenAway !== false },
|
|
557
743
|
notifyBackgroundOnly: !!draft.notifyBackgroundOnly,
|
|
558
744
|
timeoutMs: Number.isFinite(timeoutNum) && timeoutNum > 0 ? timeoutNum : 5000,
|
|
559
745
|
dnd: { start: String(draft.dndStart || '').trim(), end: String(draft.dndEnd || '').trim() },
|
|
@@ -585,6 +771,9 @@ window.__ModuleLoader__.load({
|
|
|
585
771
|
return React.createElement('span', { className: 'pn-sub' }, t('subtitle'))
|
|
586
772
|
}
|
|
587
773
|
const page = !!(props && props.view === 'page')
|
|
774
|
+
// On the plugin page (view === 'page') the head button is hidden, so the
|
|
775
|
+
// body must not depend on the accordion state — always show the form there.
|
|
776
|
+
const expanded = page || open
|
|
588
777
|
|
|
589
778
|
return React.createElement(page ? 'div' : 'li', { className: page ? 'pn-page' : 'pn-card' },
|
|
590
779
|
React.createElement('button', { type: 'button', className: 'pn-head', style: page ? { display: 'none' } : undefined, 'aria-expanded': page ? true : open, onClick: () => setOpen((v) => !v) },
|
|
@@ -594,7 +783,7 @@ window.__ModuleLoader__.load({
|
|
|
594
783
|
),
|
|
595
784
|
React.createElement(Chevron, { open }),
|
|
596
785
|
),
|
|
597
|
-
|
|
786
|
+
expanded ? React.createElement('div', { className: 'pn-body' },
|
|
598
787
|
status === 'loading'
|
|
599
788
|
? React.createElement('p', { className: 'pn-hint', style: { padding: '12px 0' } }, t('settings.loading'))
|
|
600
789
|
: status !== 'ready'
|
|
@@ -649,6 +838,22 @@ window.__ModuleLoader__.load({
|
|
|
649
838
|
? React.createElement('button', { type: 'button', className: 'pn-btn-action', onClick: requestPermission }, t('btn.reqPermission'))
|
|
650
839
|
: React.createElement('span', { className: 'pn-hint' }, permState === 'granted' ? t('perm.granted') : t('perm.denied')),
|
|
651
840
|
),
|
|
841
|
+
React.createElement('div', { className: 'pn-field' },
|
|
842
|
+
React.createElement('span', { className: 'pn-label' }, t('webpush.section')),
|
|
843
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
844
|
+
React.createElement('input', { type: 'checkbox', id: 'pn-webpush', checked: !!(draft && draft.enableWebPush), onChange: (e) => setDraft({ ...draft, enableWebPush: e.target.checked }) }),
|
|
845
|
+
React.createElement('label', { htmlFor: 'pn-webpush', className: 'pn-label' }, t('field.enableWebPush')),
|
|
846
|
+
),
|
|
847
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
848
|
+
React.createElement('input', { type: 'checkbox', id: 'pn-wponlyaway', checked: (draft && draft.webPushOnlyWhenAway) !== false, onChange: (e) => setDraft({ ...draft, webPushOnlyWhenAway: e.target.checked }) }),
|
|
849
|
+
React.createElement('label', { htmlFor: 'pn-wponlyaway', className: 'pn-label' }, t('field.webPushOnlyWhenAway')),
|
|
850
|
+
),
|
|
851
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
|
|
852
|
+
React.createElement('button', { type: 'button', className: 'pn-btn-action', onClick: onEnableWebPush }, t('btn.webpushEnable')),
|
|
853
|
+
React.createElement('button', { type: 'button', className: 'pn-btn-action', onClick: onTestPush }, t('btn.testPush')),
|
|
854
|
+
webPushStatusText ? React.createElement('span', { className: 'pn-hint' }, webPushStatusText) : null,
|
|
855
|
+
),
|
|
856
|
+
),
|
|
652
857
|
React.createElement('div', { className: 'pn-field', style: { flexDirection: 'row', alignItems: 'center', gap: 8 } },
|
|
653
858
|
React.createElement('input', { type: 'checkbox', id: 'pn-bgonly', checked: !!(draft && draft.notifyBackgroundOnly), onChange: (e) => setDraft({ ...draft, notifyBackgroundOnly: e.target.checked }) }),
|
|
654
859
|
React.createElement('label', { htmlFor: 'pn-bgonly', className: 'pn-label' }, t('field.notifyBackgroundOnly')),
|
|
@@ -699,7 +904,7 @@ window.__ModuleLoader__.load({
|
|
|
699
904
|
})
|
|
700
905
|
ctx.slots.inject('settings.plugin.item', () => {
|
|
701
906
|
ctx.slots.register(
|
|
702
|
-
{ name: 'settings.plugin.item', key:
|
|
907
|
+
{ name: 'settings.plugin.item', key: ROW_ID, locale: NS, order: 40, inject: () => ({ ctx }) },
|
|
703
908
|
(props) => React.createElement(NotifyCard, { ...props, ctx }),
|
|
704
909
|
)
|
|
705
910
|
})
|
|
@@ -710,7 +915,9 @@ window.__ModuleLoader__.load({
|
|
|
710
915
|
let currentSettings = {}
|
|
711
916
|
if (ctx.configForms) {
|
|
712
917
|
try {
|
|
713
|
-
const
|
|
918
|
+
const candidates = [ctx.configForms.get(ROW_ID), ctx.configForms.get(NS)]
|
|
919
|
+
const ready = candidates.find((s) => s && s.getSnapshot && s.getSnapshot().status === 'ready')
|
|
920
|
+
const scope = ready || candidates[0]
|
|
714
921
|
if (scope && typeof scope.subscribe === 'function') {
|
|
715
922
|
scope.subscribe(() => {
|
|
716
923
|
const snap = scope.getSnapshot ? scope.getSnapshot() : null
|
|
@@ -735,6 +942,31 @@ window.__ModuleLoader__.load({
|
|
|
735
942
|
} else {
|
|
736
943
|
startSseListener(ctx, t, () => currentSettings)
|
|
737
944
|
}
|
|
945
|
+
|
|
946
|
+
// Web push: a notification click that found this live window jumps it here.
|
|
947
|
+
try {
|
|
948
|
+
if (typeof navigator !== 'undefined' && navigator.serviceWorker && typeof navigator.serviceWorker.addEventListener === 'function') {
|
|
949
|
+
ctx.effect(() => {
|
|
950
|
+
const onMessage = (e) => {
|
|
951
|
+
const d = e && e.data
|
|
952
|
+
if (d && d.type === 'dsh-notify-jump' && d.sessionId) navigateToSession(ctx, d.sessionId)
|
|
953
|
+
}
|
|
954
|
+
navigator.serviceWorker.addEventListener('message', onMessage)
|
|
955
|
+
return () => {
|
|
956
|
+
try { navigator.serviceWorker.removeEventListener('message', onMessage) } catch (_err) { /* ignore */ }
|
|
957
|
+
}
|
|
958
|
+
}, 'dsh-plugin-notify: sw messages')
|
|
959
|
+
}
|
|
960
|
+
} catch (_err) {
|
|
961
|
+
// best effort: service worker messaging unavailable
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// A push notification that opened a fresh window lands here: ?dshNotifySession=<id>.
|
|
965
|
+
try {
|
|
966
|
+
consumeDeepLink(ctx)
|
|
967
|
+
} catch (_err) {
|
|
968
|
+
// best effort: deep-link consumption is optional
|
|
969
|
+
}
|
|
738
970
|
}
|
|
739
971
|
|
|
740
972
|
module.exports = { apply, inject: ['slots', 'locale', 'configForms'] }
|