@open-mercato/channel-apns 0.6.8-develop.6985.1.fb93574faa
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/.turbo/turbo-build.log +2 -0
- package/AGENTS.md +32 -0
- package/build.mjs +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/channel_apns/__integration__/TC-CHANNEL-PUSH-003.spec.js +47 -0
- package/dist/modules/channel_apns/__integration__/TC-CHANNEL-PUSH-003.spec.js.map +7 -0
- package/dist/modules/channel_apns/__integration__/TC-CHANNEL-PUSH-006.meta.js +7 -0
- package/dist/modules/channel_apns/__integration__/TC-CHANNEL-PUSH-006.meta.js.map +7 -0
- package/dist/modules/channel_apns/__integration__/TC-CHANNEL-PUSH-006.spec.js +104 -0
- package/dist/modules/channel_apns/__integration__/TC-CHANNEL-PUSH-006.spec.js.map +7 -0
- package/dist/modules/channel_apns/acl.js +10 -0
- package/dist/modules/channel_apns/acl.js.map +7 -0
- package/dist/modules/channel_apns/di.js +24 -0
- package/dist/modules/channel_apns/di.js.map +7 -0
- package/dist/modules/channel_apns/index.js +9 -0
- package/dist/modules/channel_apns/index.js.map +7 -0
- package/dist/modules/channel_apns/integration.js +76 -0
- package/dist/modules/channel_apns/integration.js.map +7 -0
- package/dist/modules/channel_apns/lib/adapter.js +117 -0
- package/dist/modules/channel_apns/lib/adapter.js.map +7 -0
- package/dist/modules/channel_apns/lib/credentials.js +42 -0
- package/dist/modules/channel_apns/lib/credentials.js.map +7 -0
- package/dist/modules/channel_apns/lib/fake-provider.js +30 -0
- package/dist/modules/channel_apns/lib/fake-provider.js.map +7 -0
- package/dist/modules/channel_apns/lib/health.js +10 -0
- package/dist/modules/channel_apns/lib/health.js.map +7 -0
- package/dist/modules/channel_apns/setup.js +25 -0
- package/dist/modules/channel_apns/setup.js.map +7 -0
- package/dist/modules/channel_apns/widgets/injection/connect/widget.client.js +225 -0
- package/dist/modules/channel_apns/widgets/injection/connect/widget.client.js.map +7 -0
- package/dist/modules/channel_apns/widgets/injection/connect/widget.js +17 -0
- package/dist/modules/channel_apns/widgets/injection/connect/widget.js.map +7 -0
- package/dist/modules/channel_apns/widgets/injection-table.js +15 -0
- package/dist/modules/channel_apns/widgets/injection-table.js.map +7 -0
- package/jest.config.cjs +34 -0
- package/package.json +96 -0
- package/src/index.ts +1 -0
- package/src/modules/channel_apns/__integration__/TC-CHANNEL-PUSH-003.spec.ts +67 -0
- package/src/modules/channel_apns/__integration__/TC-CHANNEL-PUSH-006.meta.ts +3 -0
- package/src/modules/channel_apns/__integration__/TC-CHANNEL-PUSH-006.spec.ts +145 -0
- package/src/modules/channel_apns/acl.ts +6 -0
- package/src/modules/channel_apns/di.ts +26 -0
- package/src/modules/channel_apns/index.ts +6 -0
- package/src/modules/channel_apns/integration.ts +77 -0
- package/src/modules/channel_apns/lib/__tests__/adapter.test.ts +158 -0
- package/src/modules/channel_apns/lib/__tests__/apnsTestKey.ts +21 -0
- package/src/modules/channel_apns/lib/__tests__/credentials.test.ts +98 -0
- package/src/modules/channel_apns/lib/__tests__/fake-provider.test.ts +93 -0
- package/src/modules/channel_apns/lib/__tests__/message-golden.test.ts +101 -0
- package/src/modules/channel_apns/lib/adapter.ts +199 -0
- package/src/modules/channel_apns/lib/credentials.ts +88 -0
- package/src/modules/channel_apns/lib/fake-provider.ts +54 -0
- package/src/modules/channel_apns/lib/health.ts +13 -0
- package/src/modules/channel_apns/setup.ts +31 -0
- package/src/modules/channel_apns/widgets/injection/connect/widget.client.tsx +251 -0
- package/src/modules/channel_apns/widgets/injection/connect/widget.ts +16 -0
- package/src/modules/channel_apns/widgets/injection-table.ts +13 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +7 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
7
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
8
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
9
|
+
import {
|
|
10
|
+
resolvePushConnectErrorMessage,
|
|
11
|
+
resolvePushConnectFieldErrors,
|
|
12
|
+
} from '@open-mercato/core/modules/communication_channels/lib/push-connect-error'
|
|
13
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
14
|
+
import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
|
|
15
|
+
import {
|
|
16
|
+
Dialog,
|
|
17
|
+
DialogContent,
|
|
18
|
+
DialogFooter,
|
|
19
|
+
DialogHeader,
|
|
20
|
+
DialogTitle,
|
|
21
|
+
} from '@open-mercato/ui/primitives/dialog'
|
|
22
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
23
|
+
import { Label } from '@open-mercato/ui/primitives/label'
|
|
24
|
+
import { Textarea } from '@open-mercato/ui/primitives/textarea'
|
|
25
|
+
|
|
26
|
+
type WidgetContext = Record<string, unknown> & {
|
|
27
|
+
reload?: () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ConnectResponse = {
|
|
31
|
+
channelId?: string
|
|
32
|
+
error?: string
|
|
33
|
+
code?: string
|
|
34
|
+
fieldErrors?: Record<string, string>
|
|
35
|
+
fieldErrorCodes?: Record<string, string>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type FormState = {
|
|
39
|
+
displayName: string
|
|
40
|
+
p8Key: string
|
|
41
|
+
keyId: string
|
|
42
|
+
teamId: string
|
|
43
|
+
bundleId: string
|
|
44
|
+
production: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const INITIAL_FORM: FormState = {
|
|
48
|
+
displayName: 'Apple Push Notification service',
|
|
49
|
+
p8Key: '',
|
|
50
|
+
keyId: '',
|
|
51
|
+
teamId: '',
|
|
52
|
+
bundleId: '',
|
|
53
|
+
production: false,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default function ConnectApnsWidget({
|
|
57
|
+
context,
|
|
58
|
+
}: InjectionWidgetComponentProps<Record<string, unknown>, Record<string, unknown>>) {
|
|
59
|
+
const t = useT()
|
|
60
|
+
const widgetContext = context as WidgetContext | undefined
|
|
61
|
+
const [open, setOpen] = React.useState(false)
|
|
62
|
+
const [pending, setPending] = React.useState(false)
|
|
63
|
+
const [form, setForm] = React.useState<FormState>(INITIAL_FORM)
|
|
64
|
+
const [fieldErrors, setFieldErrors] = React.useState<Record<string, string>>({})
|
|
65
|
+
const { runMutation, retryLastMutation } = useGuardedMutation({
|
|
66
|
+
contextId: 'channel-apns-connect',
|
|
67
|
+
blockedMessage: t('communication_channels.push.connect.blocked', 'Connection blocked by validation'),
|
|
68
|
+
})
|
|
69
|
+
const mutationContext = React.useMemo(
|
|
70
|
+
() => ({ providerKey: 'apns', retryLastMutation }),
|
|
71
|
+
[retryLastMutation],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const update = React.useCallback(
|
|
75
|
+
<K extends keyof FormState>(key: K, value: FormState[K]) => {
|
|
76
|
+
setForm((current) => ({ ...current, [key]: value }))
|
|
77
|
+
setFieldErrors((current) => {
|
|
78
|
+
if (!current[key]) return current
|
|
79
|
+
const next = { ...current }
|
|
80
|
+
delete next[key]
|
|
81
|
+
return next
|
|
82
|
+
})
|
|
83
|
+
},
|
|
84
|
+
[],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const submit = React.useCallback(async () => {
|
|
88
|
+
if (pending) return
|
|
89
|
+
setPending(true)
|
|
90
|
+
setFieldErrors({})
|
|
91
|
+
const displayName = form.displayName.trim() || 'Apple Push Notification service'
|
|
92
|
+
try {
|
|
93
|
+
const response = await runMutation({
|
|
94
|
+
context: mutationContext,
|
|
95
|
+
mutationPayload: { providerKey: 'apns', displayName },
|
|
96
|
+
operation: () =>
|
|
97
|
+
apiCall<ConnectResponse>('/api/communication_channels/channels/connect/tenant-credentials', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'content-type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
providerKey: 'apns',
|
|
102
|
+
displayName,
|
|
103
|
+
credentials: {
|
|
104
|
+
p8Key: form.p8Key.trim(),
|
|
105
|
+
keyId: form.keyId.trim(),
|
|
106
|
+
teamId: form.teamId.trim(),
|
|
107
|
+
bundleId: form.bundleId.trim(),
|
|
108
|
+
production: form.production,
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
}),
|
|
112
|
+
})
|
|
113
|
+
const body = response.result as ConnectResponse | undefined
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
setFieldErrors(resolvePushConnectFieldErrors(t, body))
|
|
116
|
+
flash(resolvePushConnectErrorMessage(t, body), 'error')
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
flash(t('communication_channels.push.connect.connected', 'Push provider connected.'), 'success')
|
|
120
|
+
setOpen(false)
|
|
121
|
+
setForm(INITIAL_FORM)
|
|
122
|
+
widgetContext?.reload?.()
|
|
123
|
+
} finally {
|
|
124
|
+
setPending(false)
|
|
125
|
+
}
|
|
126
|
+
}, [form, mutationContext, pending, runMutation, t, widgetContext])
|
|
127
|
+
|
|
128
|
+
const onDialogKeyDown = React.useCallback(
|
|
129
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
130
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
131
|
+
event.preventDefault()
|
|
132
|
+
void submit()
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
[submit],
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<>
|
|
140
|
+
<Button type="button" variant="outline" onClick={() => setOpen(true)}>
|
|
141
|
+
{t('communication_channels.push.connect.button.apns', 'Connect APNs')}
|
|
142
|
+
</Button>
|
|
143
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
144
|
+
<DialogContent onKeyDown={onDialogKeyDown}>
|
|
145
|
+
<DialogHeader>
|
|
146
|
+
<DialogTitle>
|
|
147
|
+
{t('communication_channels.push.connect.title.apns', 'Connect Apple Push Notification service')}
|
|
148
|
+
</DialogTitle>
|
|
149
|
+
</DialogHeader>
|
|
150
|
+
|
|
151
|
+
<div className="grid gap-4 py-2">
|
|
152
|
+
<p className="text-sm text-muted-foreground">
|
|
153
|
+
{t(
|
|
154
|
+
'communication_channels.push.connect.description',
|
|
155
|
+
"This is a shared, tenant-wide channel — every user's devices in this workspace are served by it.",
|
|
156
|
+
)}
|
|
157
|
+
</p>
|
|
158
|
+
<Field
|
|
159
|
+
label={t('communication_channels.push.connect.displayName', 'Display name')}
|
|
160
|
+
error={fieldErrors.displayName}
|
|
161
|
+
>
|
|
162
|
+
<Input
|
|
163
|
+
value={form.displayName}
|
|
164
|
+
onChange={(event) => update('displayName', event.target.value)}
|
|
165
|
+
aria-invalid={Boolean(fieldErrors.displayName)}
|
|
166
|
+
/>
|
|
167
|
+
</Field>
|
|
168
|
+
<Field
|
|
169
|
+
label={t('communication_channels.push.connect.fields.apns.p8Key', 'APNs .p8 key')}
|
|
170
|
+
error={fieldErrors.p8Key}
|
|
171
|
+
>
|
|
172
|
+
<Textarea
|
|
173
|
+
rows={6}
|
|
174
|
+
value={form.p8Key}
|
|
175
|
+
onChange={(event) => update('p8Key', event.target.value)}
|
|
176
|
+
aria-invalid={Boolean(fieldErrors.p8Key)}
|
|
177
|
+
className="font-mono text-xs"
|
|
178
|
+
/>
|
|
179
|
+
</Field>
|
|
180
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
181
|
+
<Field
|
|
182
|
+
label={t('communication_channels.push.connect.fields.apns.keyId', 'Key ID')}
|
|
183
|
+
error={fieldErrors.keyId}
|
|
184
|
+
>
|
|
185
|
+
<Input
|
|
186
|
+
value={form.keyId}
|
|
187
|
+
onChange={(event) => update('keyId', event.target.value)}
|
|
188
|
+
aria-invalid={Boolean(fieldErrors.keyId)}
|
|
189
|
+
/>
|
|
190
|
+
</Field>
|
|
191
|
+
<Field
|
|
192
|
+
label={t('communication_channels.push.connect.fields.apns.teamId', 'Team ID')}
|
|
193
|
+
error={fieldErrors.teamId}
|
|
194
|
+
>
|
|
195
|
+
<Input
|
|
196
|
+
value={form.teamId}
|
|
197
|
+
onChange={(event) => update('teamId', event.target.value)}
|
|
198
|
+
aria-invalid={Boolean(fieldErrors.teamId)}
|
|
199
|
+
/>
|
|
200
|
+
</Field>
|
|
201
|
+
</div>
|
|
202
|
+
<Field
|
|
203
|
+
label={t('communication_channels.push.connect.fields.apns.bundleId', 'Bundle ID')}
|
|
204
|
+
error={fieldErrors.bundleId}
|
|
205
|
+
>
|
|
206
|
+
<Input
|
|
207
|
+
value={form.bundleId}
|
|
208
|
+
onChange={(event) => update('bundleId', event.target.value)}
|
|
209
|
+
aria-invalid={Boolean(fieldErrors.bundleId)}
|
|
210
|
+
/>
|
|
211
|
+
</Field>
|
|
212
|
+
<label className="flex items-center gap-2">
|
|
213
|
+
<Checkbox
|
|
214
|
+
checked={form.production}
|
|
215
|
+
onCheckedChange={(value) => update('production', value === true)}
|
|
216
|
+
/>
|
|
217
|
+
<Label asChild>
|
|
218
|
+
<span>
|
|
219
|
+
{t('communication_channels.push.connect.fields.apns.production', 'Production environment')}
|
|
220
|
+
</span>
|
|
221
|
+
</Label>
|
|
222
|
+
</label>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<DialogFooter>
|
|
226
|
+
<Button type="button" variant="outline" onClick={() => setOpen(false)} disabled={pending}>
|
|
227
|
+
{t('communication_channels.push.connect.cancel', 'Cancel')}
|
|
228
|
+
</Button>
|
|
229
|
+
<Button type="button" onClick={() => void submit()} disabled={pending}>
|
|
230
|
+
{pending
|
|
231
|
+
? t('communication_channels.push.connect.connecting', 'Connecting…')
|
|
232
|
+
: t('communication_channels.push.connect.save', 'Connect')}
|
|
233
|
+
</Button>
|
|
234
|
+
</DialogFooter>
|
|
235
|
+
</DialogContent>
|
|
236
|
+
</Dialog>
|
|
237
|
+
</>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function Field(props: { label: string; error?: string; children: React.ReactNode }) {
|
|
242
|
+
return (
|
|
243
|
+
<label className="grid gap-1.5">
|
|
244
|
+
<Label asChild>
|
|
245
|
+
<span>{props.label}</span>
|
|
246
|
+
</Label>
|
|
247
|
+
{props.children}
|
|
248
|
+
{props.error ? <span className="text-xs text-destructive">{props.error}</span> : null}
|
|
249
|
+
</label>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
|
+
import ConnectApnsWidget from './widget.client'
|
|
3
|
+
|
|
4
|
+
const widget: InjectionWidgetModule<Record<string, unknown>, Record<string, unknown>> = {
|
|
5
|
+
metadata: {
|
|
6
|
+
id: 'channel_apns.injection.connect',
|
|
7
|
+
title: 'Connect APNs',
|
|
8
|
+
description: 'Connects a tenant-wide Apple Push Notification service push channel.',
|
|
9
|
+
features: ['communication_channels.connect_tenant_channel'],
|
|
10
|
+
priority: 90,
|
|
11
|
+
enabled: true,
|
|
12
|
+
},
|
|
13
|
+
Widget: ConnectApnsWidget,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default widget
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
|
+
|
|
3
|
+
export const injectionTable: ModuleInjectionTable = {
|
|
4
|
+
// Tenant-wide connect entry on the shared channels admin DataTable toolbar.
|
|
5
|
+
'data-table:communication_channels.channels:toolbar': [
|
|
6
|
+
{
|
|
7
|
+
widgetId: 'channel_apns.injection.connect',
|
|
8
|
+
priority: 90,
|
|
9
|
+
},
|
|
10
|
+
],
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default injectionTable
|
package/tsconfig.json
ADDED