@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,42 @@
|
|
|
1
|
+
import { createPrivateKey } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
|
|
4
|
+
import {
|
|
5
|
+
PUSH_CREDENTIAL_ERROR_INVALID_BUNDLE_ID,
|
|
6
|
+
PUSH_CREDENTIAL_ERROR_INVALID_KEY_ID,
|
|
7
|
+
PUSH_CREDENTIAL_ERROR_INVALID_P8,
|
|
8
|
+
PUSH_CREDENTIAL_ERROR_INVALID_TEAM_ID,
|
|
9
|
+
PUSH_CREDENTIAL_ERROR_REQUIRED
|
|
10
|
+
} from "@open-mercato/core/modules/communication_channels/lib/push-credential-errors";
|
|
11
|
+
const APPLE_TEN_CHAR_ID = /^[A-Za-z0-9]{10}$/;
|
|
12
|
+
const BUNDLE_ID = /^[A-Za-z0-9][A-Za-z0-9-]*(\.[A-Za-z0-9][A-Za-z0-9-]*)+$/;
|
|
13
|
+
function isParseablePrivateKey(value) {
|
|
14
|
+
try {
|
|
15
|
+
createPrivateKey(value);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const apnsCredentialsSchema = z.object({
|
|
22
|
+
p8Key: z.string().min(1, PUSH_CREDENTIAL_ERROR_REQUIRED).refine(isParseablePrivateKey, PUSH_CREDENTIAL_ERROR_INVALID_P8),
|
|
23
|
+
keyId: z.string().min(1, PUSH_CREDENTIAL_ERROR_REQUIRED).regex(APPLE_TEN_CHAR_ID, PUSH_CREDENTIAL_ERROR_INVALID_KEY_ID),
|
|
24
|
+
teamId: z.string().min(1, PUSH_CREDENTIAL_ERROR_REQUIRED).regex(APPLE_TEN_CHAR_ID, PUSH_CREDENTIAL_ERROR_INVALID_TEAM_ID),
|
|
25
|
+
bundleId: z.string().min(1, PUSH_CREDENTIAL_ERROR_REQUIRED).regex(BUNDLE_ID, PUSH_CREDENTIAL_ERROR_INVALID_BUNDLE_ID),
|
|
26
|
+
production: z.union([z.boolean(), z.string()]).optional()
|
|
27
|
+
}).passthrough();
|
|
28
|
+
function resolveApnsCredentials(credentials) {
|
|
29
|
+
const production = typeof credentials.production === "boolean" ? credentials.production : parseBooleanWithDefault(credentials.production, false);
|
|
30
|
+
return {
|
|
31
|
+
p8Key: credentials.p8Key,
|
|
32
|
+
keyId: credentials.keyId,
|
|
33
|
+
teamId: credentials.teamId,
|
|
34
|
+
bundleId: credentials.bundleId,
|
|
35
|
+
production
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
apnsCredentialsSchema,
|
|
40
|
+
resolveApnsCredentials
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/channel_apns/lib/credentials.ts"],
|
|
4
|
+
"sourcesContent": ["import { createPrivateKey } from 'node:crypto'\nimport { z } from 'zod'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport {\n PUSH_CREDENTIAL_ERROR_INVALID_BUNDLE_ID,\n PUSH_CREDENTIAL_ERROR_INVALID_KEY_ID,\n PUSH_CREDENTIAL_ERROR_INVALID_P8,\n PUSH_CREDENTIAL_ERROR_INVALID_TEAM_ID,\n PUSH_CREDENTIAL_ERROR_REQUIRED,\n} from '@open-mercato/core/modules/communication_channels/lib/push-credential-errors'\n\n/** Apple issues both Key IDs and Team IDs as exactly 10 alphanumeric characters. */\nconst APPLE_TEN_CHAR_ID = /^[A-Za-z0-9]{10}$/\n/** Reverse-DNS app identifier, e.g. `com.example.app`; also the APNs `topic`. */\nconst BUNDLE_ID = /^[A-Za-z0-9][A-Za-z0-9-]*(\\.[A-Za-z0-9][A-Za-z0-9-]*)+$/\n\n/**\n * Structurally verify Apple's `.p8` signing key without contacting APNs.\n * `createPrivateKey` parses the PEM and rejects anything that is not a readable\n * private key, which is what makes a pasted-by-mistake string fail at connect\n * time instead of silently producing a channel that can never deliver.\n *\n * This proves the key is well-formed, NOT that Apple accepts it \u2014 a\n * syntactically valid key from the wrong developer account still connects. Live\n * verification is tracked separately.\n */\nfunction isParseablePrivateKey(value: string): boolean {\n try {\n createPrivateKey(value)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Tenant-level APNs credentials persisted on `IntegrationCredentials` for provider\n * `channel_apns`. Token-based auth (Apple's `.p8` key) \u2014 `p8Key` is the PEM\n * contents (stored encrypted at rest), `keyId`/`teamId` identify the key, and\n * `bundleId` is the app's APNs `topic`. `production` selects the APNs host\n * (sandbox by default).\n */\nexport const apnsCredentialsSchema = z\n .object({\n p8Key: z\n .string()\n .min(1, PUSH_CREDENTIAL_ERROR_REQUIRED)\n .refine(isParseablePrivateKey, PUSH_CREDENTIAL_ERROR_INVALID_P8),\n keyId: z\n .string()\n .min(1, PUSH_CREDENTIAL_ERROR_REQUIRED)\n .regex(APPLE_TEN_CHAR_ID, PUSH_CREDENTIAL_ERROR_INVALID_KEY_ID),\n teamId: z\n .string()\n .min(1, PUSH_CREDENTIAL_ERROR_REQUIRED)\n .regex(APPLE_TEN_CHAR_ID, PUSH_CREDENTIAL_ERROR_INVALID_TEAM_ID),\n bundleId: z\n .string()\n .min(1, PUSH_CREDENTIAL_ERROR_REQUIRED)\n .regex(BUNDLE_ID, PUSH_CREDENTIAL_ERROR_INVALID_BUNDLE_ID),\n production: z.union([z.boolean(), z.string()]).optional(),\n })\n .passthrough()\n\nexport type ApnsCredentials = z.infer<typeof apnsCredentialsSchema>\n\nexport interface ApnsResolvedCredentials {\n p8Key: string\n keyId: string\n teamId: string\n bundleId: string\n production: boolean\n}\n\n/** Resolve validated credentials into the strongly-typed send config (parsing the production flag). */\nexport function resolveApnsCredentials(credentials: ApnsCredentials): ApnsResolvedCredentials {\n const production =\n typeof credentials.production === 'boolean'\n ? credentials.production\n : parseBooleanWithDefault(credentials.production, false)\n return {\n p8Key: credentials.p8Key,\n keyId: credentials.keyId,\n teamId: credentials.teamId,\n bundleId: credentials.bundleId,\n production,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,wBAAwB;AACjC,SAAS,SAAS;AAClB,SAAS,+BAA+B;AACxC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,MAAM,oBAAoB;AAE1B,MAAM,YAAY;AAYlB,SAAS,sBAAsB,OAAwB;AACrD,MAAI;AACF,qBAAiB,KAAK;AACtB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASO,MAAM,wBAAwB,EAClC,OAAO;AAAA,EACN,OAAO,EACJ,OAAO,EACP,IAAI,GAAG,8BAA8B,EACrC,OAAO,uBAAuB,gCAAgC;AAAA,EACjE,OAAO,EACJ,OAAO,EACP,IAAI,GAAG,8BAA8B,EACrC,MAAM,mBAAmB,oCAAoC;AAAA,EAChE,QAAQ,EACL,OAAO,EACP,IAAI,GAAG,8BAA8B,EACrC,MAAM,mBAAmB,qCAAqC;AAAA,EACjE,UAAU,EACP,OAAO,EACP,IAAI,GAAG,8BAA8B,EACrC,MAAM,WAAW,uCAAuC;AAAA,EAC3D,YAAY,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS;AAC1D,CAAC,EACA,YAAY;AAaR,SAAS,uBAAuB,aAAuD;AAC5F,QAAM,aACJ,OAAO,YAAY,eAAe,YAC9B,YAAY,aACZ,wBAAwB,YAAY,YAAY,KAAK;AAC3D,SAAO;AAAA,IACL,OAAO,YAAY;AAAA,IACnB,OAAO,YAAY;AAAA,IACnB,QAAQ,YAAY;AAAA,IACpB,UAAU,YAAY;AAAA,IACtB;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isPushFakeProvidersEnabled,
|
|
3
|
+
recordFakePush,
|
|
4
|
+
warnPushFakeProvidersActive
|
|
5
|
+
} from "@open-mercato/core/modules/push_notifications/lib/fake-provider-recorder";
|
|
6
|
+
import { buildApnsNotification, setApnsSenderFactory } from "./adapter.js";
|
|
7
|
+
async function newApnsNotification() {
|
|
8
|
+
const apnModule = await import("@parse/node-apn");
|
|
9
|
+
const apn = apnModule.default ?? apnModule;
|
|
10
|
+
const Notification = apn.Notification;
|
|
11
|
+
return new Notification();
|
|
12
|
+
}
|
|
13
|
+
function ensureApnsFakeProviderInstalled() {
|
|
14
|
+
if (!isPushFakeProvidersEnabled()) return;
|
|
15
|
+
warnPushFakeProvidersActive("apns");
|
|
16
|
+
setApnsSenderFactory(() => async (payload, token) => {
|
|
17
|
+
const note = buildApnsNotification(await newApnsNotification(), payload);
|
|
18
|
+
recordFakePush("apns", token, {
|
|
19
|
+
headers: note.headers(),
|
|
20
|
+
payload: JSON.parse(note.compile())
|
|
21
|
+
});
|
|
22
|
+
if (token.includes("unregistered")) return { ok: false, reason: "Unregistered" };
|
|
23
|
+
if (token.includes("fail")) return { ok: false, error: "fake apns transient failure" };
|
|
24
|
+
return { ok: true };
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
ensureApnsFakeProviderInstalled
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=fake-provider.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/channel_apns/lib/fake-provider.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n isPushFakeProvidersEnabled,\n recordFakePush,\n warnPushFakeProvidersActive,\n} from '@open-mercato/core/modules/push_notifications/lib/fake-provider-recorder'\nimport { buildApnsNotification, setApnsSenderFactory } from './adapter'\n\n/**\n * Network-free `@parse/node-apn` sender used ONLY by integration tests.\n *\n * Swaps the SDK client behind the adapter's existing seam, so the real adapter still runs its\n * credential resolution and its `Unregistered`/`410` \u2192 `device_unregistered` mapping. The adapter\n * itself is never replaced or re-registered.\n *\n * Unlike FCM and Expo, the APNs seam sits *above* the message builder: the sender receives the raw\n * envelope, and `buildApnsNotification(new Notification(), \u2026)` runs inside the real sender factory this\n * fake replaces. The fake therefore builds against a **real `apn.Notification`** too, and records the\n * wire form node-apn would transmit (`headers()` + the compiled `aps` payload) rather than a plain-object\n * projection the SDK never serializes. Only the network provider is faked. `.p8` parsing lives in the\n * replaced factory, so fake credentials need only a valid shape.\n *\n * Token sentinels match `push_stub`'s convention (see push-stub-adapter.ts):\n * - token containing `unregistered` \u2192 APNs' native permanent-token reason\n * - token containing `fail` \u2192 a retryable error\n * - otherwise \u2192 success\n *\n * Production safety: never installed at module import; no-op unless `OM_PUSH_FAKE_PROVIDERS` is set.\n */\ntype ApnsNotificationLike = Record<string, unknown> & {\n headers(): Record<string, unknown>\n compile(): string\n}\n\nasync function newApnsNotification(): Promise<ApnsNotificationLike> {\n const apnModule = await import('@parse/node-apn')\n const apn = (apnModule as { default?: unknown }).default ?? apnModule\n const Notification = (apn as { Notification: new () => ApnsNotificationLike }).Notification\n return new Notification()\n}\n\nexport function ensureApnsFakeProviderInstalled(): void {\n if (!isPushFakeProvidersEnabled()) return\n warnPushFakeProvidersActive('apns')\n setApnsSenderFactory(() => async (payload, token) => {\n const note = buildApnsNotification(await newApnsNotification(), payload) as ApnsNotificationLike\n recordFakePush('apns', token, {\n headers: note.headers(),\n payload: JSON.parse(note.compile()) as Record<string, unknown>,\n })\n if (token.includes('unregistered')) return { ok: false, reason: 'Unregistered' }\n if (token.includes('fail')) return { ok: false, error: 'fake apns transient failure' }\n return { ok: true }\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAuB,4BAA4B;AA4B5D,eAAe,sBAAqD;AAClE,QAAM,YAAY,MAAM,OAAO,iBAAiB;AAChD,QAAM,MAAO,UAAoC,WAAW;AAC5D,QAAM,eAAgB,IAAyD;AAC/E,SAAO,IAAI,aAAa;AAC1B;AAEO,SAAS,kCAAwC;AACtD,MAAI,CAAC,2BAA2B,EAAG;AACnC,8BAA4B,MAAM;AAClC,uBAAqB,MAAM,OAAO,SAAS,UAAU;AACnD,UAAM,OAAO,sBAAsB,MAAM,oBAAoB,GAAG,OAAO;AACvE,mBAAe,QAAQ,OAAO;AAAA,MAC5B,SAAS,KAAK,QAAQ;AAAA,MACtB,SAAS,KAAK,MAAM,KAAK,QAAQ,CAAC;AAAA,IACpC,CAAC;AACD,QAAI,MAAM,SAAS,cAAc,EAAG,QAAO,EAAE,IAAI,OAAO,QAAQ,eAAe;AAC/E,QAAI,MAAM,SAAS,MAAM,EAAG,QAAO,EAAE,IAAI,OAAO,OAAO,8BAA8B;AACrF,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB,CAAC;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { makePushClientConfigHealthCheck } from "@open-mercato/core/modules/push_notifications/lib/push-health";
|
|
2
|
+
import { apnsCredentialsSchema } from "./credentials.js";
|
|
3
|
+
const channelApnsHealthCheck = makePushClientConfigHealthCheck({
|
|
4
|
+
schema: apnsCredentialsSchema,
|
|
5
|
+
providerLabel: "APNs"
|
|
6
|
+
});
|
|
7
|
+
export {
|
|
8
|
+
channelApnsHealthCheck
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/channel_apns/lib/health.ts"],
|
|
4
|
+
"sourcesContent": ["import { makePushClientConfigHealthCheck } from '@open-mercato/core/modules/push_notifications/lib/push-health'\nimport { apnsCredentialsSchema } from './credentials'\n\n/**\n * Liveness probe for the APNs integration. The hub passes the tenant-scoped\n * credentials (.p8 key + key/team/bundle ids), so the probe confirms they are\n * present and well-formed \u2014 no network call. Per-device token validity surfaces\n * on delivery (`device_unregistered` soft-deletes).\n */\nexport const channelApnsHealthCheck = makePushClientConfigHealthCheck({\n schema: apnsCredentialsSchema,\n providerLabel: 'APNs',\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,uCAAuC;AAChD,SAAS,6BAA6B;AAQ/B,MAAM,yBAAyB,gCAAgC;AAAA,EACpE,QAAQ;AAAA,EACR,eAAe;AACjB,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasChannelAdapter,
|
|
3
|
+
registerChannelAdapter
|
|
4
|
+
} from "@open-mercato/core/modules/communication_channels/lib/adapter-registry-singleton";
|
|
5
|
+
import { getApnsChannelAdapter } from "./lib/adapter.js";
|
|
6
|
+
function ensureApnsAdapterRegistered() {
|
|
7
|
+
if (hasChannelAdapter("apns")) return;
|
|
8
|
+
registerChannelAdapter(getApnsChannelAdapter());
|
|
9
|
+
}
|
|
10
|
+
ensureApnsAdapterRegistered();
|
|
11
|
+
const setup = {
|
|
12
|
+
defaultRoleFeatures: {
|
|
13
|
+
superadmin: ["channel_apns.view", "channel_apns.configure"],
|
|
14
|
+
admin: ["channel_apns.view", "channel_apns.configure"]
|
|
15
|
+
},
|
|
16
|
+
async onTenantCreated() {
|
|
17
|
+
ensureApnsAdapterRegistered();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var setup_default = setup;
|
|
21
|
+
export {
|
|
22
|
+
setup_default as default,
|
|
23
|
+
setup
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/channel_apns/setup.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport {\n hasChannelAdapter,\n registerChannelAdapter,\n} from '@open-mercato/core/modules/communication_channels/lib/adapter-registry-singleton'\nimport { getApnsChannelAdapter } from './lib/adapter'\n\n/**\n * Register the APNs `ChannelAdapter` once per process at import time. Guarded with\n * `hasChannelAdapter` to silence the duplicate error on dev-mode HMR + repeated\n * test imports. Provider credentials (.p8 key + ids) are persisted per tenant via\n * the standard `IntegrationCredentials` flow for the `channel_apns` provider.\n */\nfunction ensureApnsAdapterRegistered(): void {\n if (hasChannelAdapter('apns')) return\n registerChannelAdapter(getApnsChannelAdapter())\n}\n\nensureApnsAdapterRegistered()\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n superadmin: ['channel_apns.view', 'channel_apns.configure'],\n admin: ['channel_apns.view', 'channel_apns.configure'],\n },\n async onTenantCreated() {\n ensureApnsAdapterRegistered()\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,6BAA6B;AAQtC,SAAS,8BAAoC;AAC3C,MAAI,kBAAkB,MAAM,EAAG;AAC/B,yBAAuB,sBAAsB,CAAC;AAChD;AAEA,4BAA4B;AAErB,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,YAAY,CAAC,qBAAqB,wBAAwB;AAAA,IAC1D,OAAO,CAAC,qBAAqB,wBAAwB;AAAA,EACvD;AAAA,EACA,MAAM,kBAAkB;AACtB,gCAA4B;AAAA,EAC9B;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
|
+
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
6
|
+
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
7
|
+
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
8
|
+
import {
|
|
9
|
+
resolvePushConnectErrorMessage,
|
|
10
|
+
resolvePushConnectFieldErrors
|
|
11
|
+
} from "@open-mercato/core/modules/communication_channels/lib/push-connect-error";
|
|
12
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
13
|
+
import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
|
|
14
|
+
import {
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogContent,
|
|
17
|
+
DialogFooter,
|
|
18
|
+
DialogHeader,
|
|
19
|
+
DialogTitle
|
|
20
|
+
} from "@open-mercato/ui/primitives/dialog";
|
|
21
|
+
import { Input } from "@open-mercato/ui/primitives/input";
|
|
22
|
+
import { Label } from "@open-mercato/ui/primitives/label";
|
|
23
|
+
import { Textarea } from "@open-mercato/ui/primitives/textarea";
|
|
24
|
+
const INITIAL_FORM = {
|
|
25
|
+
displayName: "Apple Push Notification service",
|
|
26
|
+
p8Key: "",
|
|
27
|
+
keyId: "",
|
|
28
|
+
teamId: "",
|
|
29
|
+
bundleId: "",
|
|
30
|
+
production: false
|
|
31
|
+
};
|
|
32
|
+
function ConnectApnsWidget({
|
|
33
|
+
context
|
|
34
|
+
}) {
|
|
35
|
+
const t = useT();
|
|
36
|
+
const widgetContext = context;
|
|
37
|
+
const [open, setOpen] = React.useState(false);
|
|
38
|
+
const [pending, setPending] = React.useState(false);
|
|
39
|
+
const [form, setForm] = React.useState(INITIAL_FORM);
|
|
40
|
+
const [fieldErrors, setFieldErrors] = React.useState({});
|
|
41
|
+
const { runMutation, retryLastMutation } = useGuardedMutation({
|
|
42
|
+
contextId: "channel-apns-connect",
|
|
43
|
+
blockedMessage: t("communication_channels.push.connect.blocked", "Connection blocked by validation")
|
|
44
|
+
});
|
|
45
|
+
const mutationContext = React.useMemo(
|
|
46
|
+
() => ({ providerKey: "apns", retryLastMutation }),
|
|
47
|
+
[retryLastMutation]
|
|
48
|
+
);
|
|
49
|
+
const update = React.useCallback(
|
|
50
|
+
(key, value) => {
|
|
51
|
+
setForm((current) => ({ ...current, [key]: value }));
|
|
52
|
+
setFieldErrors((current) => {
|
|
53
|
+
if (!current[key]) return current;
|
|
54
|
+
const next = { ...current };
|
|
55
|
+
delete next[key];
|
|
56
|
+
return next;
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
[]
|
|
60
|
+
);
|
|
61
|
+
const submit = React.useCallback(async () => {
|
|
62
|
+
if (pending) return;
|
|
63
|
+
setPending(true);
|
|
64
|
+
setFieldErrors({});
|
|
65
|
+
const displayName = form.displayName.trim() || "Apple Push Notification service";
|
|
66
|
+
try {
|
|
67
|
+
const response = await runMutation({
|
|
68
|
+
context: mutationContext,
|
|
69
|
+
mutationPayload: { providerKey: "apns", displayName },
|
|
70
|
+
operation: () => apiCall("/api/communication_channels/channels/connect/tenant-credentials", {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "content-type": "application/json" },
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
providerKey: "apns",
|
|
75
|
+
displayName,
|
|
76
|
+
credentials: {
|
|
77
|
+
p8Key: form.p8Key.trim(),
|
|
78
|
+
keyId: form.keyId.trim(),
|
|
79
|
+
teamId: form.teamId.trim(),
|
|
80
|
+
bundleId: form.bundleId.trim(),
|
|
81
|
+
production: form.production
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
const body = response.result;
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
setFieldErrors(resolvePushConnectFieldErrors(t, body));
|
|
89
|
+
flash(resolvePushConnectErrorMessage(t, body), "error");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
flash(t("communication_channels.push.connect.connected", "Push provider connected."), "success");
|
|
93
|
+
setOpen(false);
|
|
94
|
+
setForm(INITIAL_FORM);
|
|
95
|
+
widgetContext?.reload?.();
|
|
96
|
+
} finally {
|
|
97
|
+
setPending(false);
|
|
98
|
+
}
|
|
99
|
+
}, [form, mutationContext, pending, runMutation, t, widgetContext]);
|
|
100
|
+
const onDialogKeyDown = React.useCallback(
|
|
101
|
+
(event) => {
|
|
102
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
void submit();
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[submit]
|
|
108
|
+
);
|
|
109
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
110
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => setOpen(true), children: t("communication_channels.push.connect.button.apns", "Connect APNs") }),
|
|
111
|
+
/* @__PURE__ */ jsx(Dialog, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs(DialogContent, { onKeyDown: onDialogKeyDown, children: [
|
|
112
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: t("communication_channels.push.connect.title.apns", "Connect Apple Push Notification service") }) }),
|
|
113
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 py-2", children: [
|
|
114
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t(
|
|
115
|
+
"communication_channels.push.connect.description",
|
|
116
|
+
"This is a shared, tenant-wide channel \u2014 every user's devices in this workspace are served by it."
|
|
117
|
+
) }),
|
|
118
|
+
/* @__PURE__ */ jsx(
|
|
119
|
+
Field,
|
|
120
|
+
{
|
|
121
|
+
label: t("communication_channels.push.connect.displayName", "Display name"),
|
|
122
|
+
error: fieldErrors.displayName,
|
|
123
|
+
children: /* @__PURE__ */ jsx(
|
|
124
|
+
Input,
|
|
125
|
+
{
|
|
126
|
+
value: form.displayName,
|
|
127
|
+
onChange: (event) => update("displayName", event.target.value),
|
|
128
|
+
"aria-invalid": Boolean(fieldErrors.displayName)
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
),
|
|
133
|
+
/* @__PURE__ */ jsx(
|
|
134
|
+
Field,
|
|
135
|
+
{
|
|
136
|
+
label: t("communication_channels.push.connect.fields.apns.p8Key", "APNs .p8 key"),
|
|
137
|
+
error: fieldErrors.p8Key,
|
|
138
|
+
children: /* @__PURE__ */ jsx(
|
|
139
|
+
Textarea,
|
|
140
|
+
{
|
|
141
|
+
rows: 6,
|
|
142
|
+
value: form.p8Key,
|
|
143
|
+
onChange: (event) => update("p8Key", event.target.value),
|
|
144
|
+
"aria-invalid": Boolean(fieldErrors.p8Key),
|
|
145
|
+
className: "font-mono text-xs"
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
),
|
|
150
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
|
|
151
|
+
/* @__PURE__ */ jsx(
|
|
152
|
+
Field,
|
|
153
|
+
{
|
|
154
|
+
label: t("communication_channels.push.connect.fields.apns.keyId", "Key ID"),
|
|
155
|
+
error: fieldErrors.keyId,
|
|
156
|
+
children: /* @__PURE__ */ jsx(
|
|
157
|
+
Input,
|
|
158
|
+
{
|
|
159
|
+
value: form.keyId,
|
|
160
|
+
onChange: (event) => update("keyId", event.target.value),
|
|
161
|
+
"aria-invalid": Boolean(fieldErrors.keyId)
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
),
|
|
166
|
+
/* @__PURE__ */ jsx(
|
|
167
|
+
Field,
|
|
168
|
+
{
|
|
169
|
+
label: t("communication_channels.push.connect.fields.apns.teamId", "Team ID"),
|
|
170
|
+
error: fieldErrors.teamId,
|
|
171
|
+
children: /* @__PURE__ */ jsx(
|
|
172
|
+
Input,
|
|
173
|
+
{
|
|
174
|
+
value: form.teamId,
|
|
175
|
+
onChange: (event) => update("teamId", event.target.value),
|
|
176
|
+
"aria-invalid": Boolean(fieldErrors.teamId)
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
] }),
|
|
182
|
+
/* @__PURE__ */ jsx(
|
|
183
|
+
Field,
|
|
184
|
+
{
|
|
185
|
+
label: t("communication_channels.push.connect.fields.apns.bundleId", "Bundle ID"),
|
|
186
|
+
error: fieldErrors.bundleId,
|
|
187
|
+
children: /* @__PURE__ */ jsx(
|
|
188
|
+
Input,
|
|
189
|
+
{
|
|
190
|
+
value: form.bundleId,
|
|
191
|
+
onChange: (event) => update("bundleId", event.target.value),
|
|
192
|
+
"aria-invalid": Boolean(fieldErrors.bundleId)
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
),
|
|
197
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2", children: [
|
|
198
|
+
/* @__PURE__ */ jsx(
|
|
199
|
+
Checkbox,
|
|
200
|
+
{
|
|
201
|
+
checked: form.production,
|
|
202
|
+
onCheckedChange: (value) => update("production", value === true)
|
|
203
|
+
}
|
|
204
|
+
),
|
|
205
|
+
/* @__PURE__ */ jsx(Label, { asChild: true, children: /* @__PURE__ */ jsx("span", { children: t("communication_channels.push.connect.fields.apns.production", "Production environment") }) })
|
|
206
|
+
] })
|
|
207
|
+
] }),
|
|
208
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
209
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => setOpen(false), disabled: pending, children: t("communication_channels.push.connect.cancel", "Cancel") }),
|
|
210
|
+
/* @__PURE__ */ jsx(Button, { type: "button", onClick: () => void submit(), disabled: pending, children: pending ? t("communication_channels.push.connect.connecting", "Connecting\u2026") : t("communication_channels.push.connect.save", "Connect") })
|
|
211
|
+
] })
|
|
212
|
+
] }) })
|
|
213
|
+
] });
|
|
214
|
+
}
|
|
215
|
+
function Field(props) {
|
|
216
|
+
return /* @__PURE__ */ jsxs("label", { className: "grid gap-1.5", children: [
|
|
217
|
+
/* @__PURE__ */ jsx(Label, { asChild: true, children: /* @__PURE__ */ jsx("span", { children: props.label }) }),
|
|
218
|
+
props.children,
|
|
219
|
+
props.error ? /* @__PURE__ */ jsx("span", { className: "text-xs text-destructive", children: props.error }) : null
|
|
220
|
+
] });
|
|
221
|
+
}
|
|
222
|
+
export {
|
|
223
|
+
ConnectApnsWidget as default
|
|
224
|
+
};
|
|
225
|
+
//# sourceMappingURL=widget.client.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/channel_apns/widgets/injection/connect/widget.client.tsx"],
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport {\n resolvePushConnectErrorMessage,\n resolvePushConnectFieldErrors,\n} from '@open-mercato/core/modules/communication_channels/lib/push-connect-error'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\n\ntype WidgetContext = Record<string, unknown> & {\n reload?: () => void\n}\n\ntype ConnectResponse = {\n channelId?: string\n error?: string\n code?: string\n fieldErrors?: Record<string, string>\n fieldErrorCodes?: Record<string, string>\n}\n\ntype FormState = {\n displayName: string\n p8Key: string\n keyId: string\n teamId: string\n bundleId: string\n production: boolean\n}\n\nconst INITIAL_FORM: FormState = {\n displayName: 'Apple Push Notification service',\n p8Key: '',\n keyId: '',\n teamId: '',\n bundleId: '',\n production: false,\n}\n\nexport default function ConnectApnsWidget({\n context,\n}: InjectionWidgetComponentProps<Record<string, unknown>, Record<string, unknown>>) {\n const t = useT()\n const widgetContext = context as WidgetContext | undefined\n const [open, setOpen] = React.useState(false)\n const [pending, setPending] = React.useState(false)\n const [form, setForm] = React.useState<FormState>(INITIAL_FORM)\n const [fieldErrors, setFieldErrors] = React.useState<Record<string, string>>({})\n const { runMutation, retryLastMutation } = useGuardedMutation({\n contextId: 'channel-apns-connect',\n blockedMessage: t('communication_channels.push.connect.blocked', 'Connection blocked by validation'),\n })\n const mutationContext = React.useMemo(\n () => ({ providerKey: 'apns', retryLastMutation }),\n [retryLastMutation],\n )\n\n const update = React.useCallback(\n <K extends keyof FormState>(key: K, value: FormState[K]) => {\n setForm((current) => ({ ...current, [key]: value }))\n setFieldErrors((current) => {\n if (!current[key]) return current\n const next = { ...current }\n delete next[key]\n return next\n })\n },\n [],\n )\n\n const submit = React.useCallback(async () => {\n if (pending) return\n setPending(true)\n setFieldErrors({})\n const displayName = form.displayName.trim() || 'Apple Push Notification service'\n try {\n const response = await runMutation({\n context: mutationContext,\n mutationPayload: { providerKey: 'apns', displayName },\n operation: () =>\n apiCall<ConnectResponse>('/api/communication_channels/channels/connect/tenant-credentials', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n providerKey: 'apns',\n displayName,\n credentials: {\n p8Key: form.p8Key.trim(),\n keyId: form.keyId.trim(),\n teamId: form.teamId.trim(),\n bundleId: form.bundleId.trim(),\n production: form.production,\n },\n }),\n }),\n })\n const body = response.result as ConnectResponse | undefined\n if (!response.ok) {\n setFieldErrors(resolvePushConnectFieldErrors(t, body))\n flash(resolvePushConnectErrorMessage(t, body), 'error')\n return\n }\n flash(t('communication_channels.push.connect.connected', 'Push provider connected.'), 'success')\n setOpen(false)\n setForm(INITIAL_FORM)\n widgetContext?.reload?.()\n } finally {\n setPending(false)\n }\n }, [form, mutationContext, pending, runMutation, t, widgetContext])\n\n const onDialogKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void submit()\n }\n },\n [submit],\n )\n\n return (\n <>\n <Button type=\"button\" variant=\"outline\" onClick={() => setOpen(true)}>\n {t('communication_channels.push.connect.button.apns', 'Connect APNs')}\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent onKeyDown={onDialogKeyDown}>\n <DialogHeader>\n <DialogTitle>\n {t('communication_channels.push.connect.title.apns', 'Connect Apple Push Notification service')}\n </DialogTitle>\n </DialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'communication_channels.push.connect.description',\n \"This is a shared, tenant-wide channel \u2014 every user's devices in this workspace are served by it.\",\n )}\n </p>\n <Field\n label={t('communication_channels.push.connect.displayName', 'Display name')}\n error={fieldErrors.displayName}\n >\n <Input\n value={form.displayName}\n onChange={(event) => update('displayName', event.target.value)}\n aria-invalid={Boolean(fieldErrors.displayName)}\n />\n </Field>\n <Field\n label={t('communication_channels.push.connect.fields.apns.p8Key', 'APNs .p8 key')}\n error={fieldErrors.p8Key}\n >\n <Textarea\n rows={6}\n value={form.p8Key}\n onChange={(event) => update('p8Key', event.target.value)}\n aria-invalid={Boolean(fieldErrors.p8Key)}\n className=\"font-mono text-xs\"\n />\n </Field>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field\n label={t('communication_channels.push.connect.fields.apns.keyId', 'Key ID')}\n error={fieldErrors.keyId}\n >\n <Input\n value={form.keyId}\n onChange={(event) => update('keyId', event.target.value)}\n aria-invalid={Boolean(fieldErrors.keyId)}\n />\n </Field>\n <Field\n label={t('communication_channels.push.connect.fields.apns.teamId', 'Team ID')}\n error={fieldErrors.teamId}\n >\n <Input\n value={form.teamId}\n onChange={(event) => update('teamId', event.target.value)}\n aria-invalid={Boolean(fieldErrors.teamId)}\n />\n </Field>\n </div>\n <Field\n label={t('communication_channels.push.connect.fields.apns.bundleId', 'Bundle ID')}\n error={fieldErrors.bundleId}\n >\n <Input\n value={form.bundleId}\n onChange={(event) => update('bundleId', event.target.value)}\n aria-invalid={Boolean(fieldErrors.bundleId)}\n />\n </Field>\n <label className=\"flex items-center gap-2\">\n <Checkbox\n checked={form.production}\n onCheckedChange={(value) => update('production', value === true)}\n />\n <Label asChild>\n <span>\n {t('communication_channels.push.connect.fields.apns.production', 'Production environment')}\n </span>\n </Label>\n </label>\n </div>\n\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => setOpen(false)} disabled={pending}>\n {t('communication_channels.push.connect.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" onClick={() => void submit()} disabled={pending}>\n {pending\n ? t('communication_channels.push.connect.connecting', 'Connecting\u2026')\n : t('communication_channels.push.connect.save', 'Connect')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n\nfunction Field(props: { label: string; error?: string; children: React.ReactNode }) {\n return (\n <label className=\"grid gap-1.5\">\n <Label asChild>\n <span>{props.label}</span>\n </Label>\n {props.children}\n {props.error ? <span className=\"text-xs text-destructive\">{props.error}</span> : null}\n </label>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import ConnectApnsWidget from "./widget.client.js";
|
|
2
|
+
const widget = {
|
|
3
|
+
metadata: {
|
|
4
|
+
id: "channel_apns.injection.connect",
|
|
5
|
+
title: "Connect APNs",
|
|
6
|
+
description: "Connects a tenant-wide Apple Push Notification service push channel.",
|
|
7
|
+
features: ["communication_channels.connect_tenant_channel"],
|
|
8
|
+
priority: 90,
|
|
9
|
+
enabled: true
|
|
10
|
+
},
|
|
11
|
+
Widget: ConnectApnsWidget
|
|
12
|
+
};
|
|
13
|
+
var widget_default = widget;
|
|
14
|
+
export {
|
|
15
|
+
widget_default as default
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=widget.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/channel_apns/widgets/injection/connect/widget.ts"],
|
|
4
|
+
"sourcesContent": ["import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'\nimport ConnectApnsWidget from './widget.client'\n\nconst widget: InjectionWidgetModule<Record<string, unknown>, Record<string, unknown>> = {\n metadata: {\n id: 'channel_apns.injection.connect',\n title: 'Connect APNs',\n description: 'Connects a tenant-wide Apple Push Notification service push channel.',\n features: ['communication_channels.connect_tenant_channel'],\n priority: 90,\n enabled: true,\n },\n Widget: ConnectApnsWidget,\n}\n\nexport default widget\n"],
|
|
5
|
+
"mappings": "AACA,OAAO,uBAAuB;AAE9B,MAAM,SAAkF;AAAA,EACtF,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU,CAAC,+CAA+C;AAAA,IAC1D,UAAU;AAAA,IACV,SAAS;AAAA,EACX;AAAA,EACA,QAAQ;AACV;AAEA,IAAO,iBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const injectionTable = {
|
|
2
|
+
// Tenant-wide connect entry on the shared channels admin DataTable toolbar.
|
|
3
|
+
"data-table:communication_channels.channels:toolbar": [
|
|
4
|
+
{
|
|
5
|
+
widgetId: "channel_apns.injection.connect",
|
|
6
|
+
priority: 90
|
|
7
|
+
}
|
|
8
|
+
]
|
|
9
|
+
};
|
|
10
|
+
var injection_table_default = injectionTable;
|
|
11
|
+
export {
|
|
12
|
+
injection_table_default as default,
|
|
13
|
+
injectionTable
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=injection-table.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/channel_apns/widgets/injection-table.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'\n\nexport const injectionTable: ModuleInjectionTable = {\n // Tenant-wide connect entry on the shared channels admin DataTable toolbar.\n 'data-table:communication_channels.channels:toolbar': [\n {\n widgetId: 'channel_apns.injection.connect',\n priority: 90,\n },\n ],\n}\n\nexport default injectionTable\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,iBAAuC;AAAA;AAAA,EAElD,sDAAsD;AAAA,IACpD;AAAA,MACE,UAAU;AAAA,MACV,UAAU;AAAA,IACZ;AAAA,EACF;AACF;AAEA,IAAO,0BAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/jest.config.cjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
const base = require('../../jest.config.base.cjs')
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
...base,
|
|
6
|
+
testEnvironment: 'node',
|
|
7
|
+
watchman: false,
|
|
8
|
+
rootDir: '.',
|
|
9
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
|
10
|
+
moduleNameMapper: {
|
|
11
|
+
'^@open-mercato/channel-apns/(.*)$': '<rootDir>/src/$1',
|
|
12
|
+
'^@open-mercato/core/(.*)$': '<rootDir>/../core/src/$1',
|
|
13
|
+
'^@open-mercato/shared/(.*)$': '<rootDir>/../shared/src/$1',
|
|
14
|
+
'^@open-mercato/queue/(.*)$': '<rootDir>/../queue/src/$1',
|
|
15
|
+
'^@open-mercato/ui/(.*)$': '<rootDir>/../ui/src/$1',
|
|
16
|
+
},
|
|
17
|
+
transform: {
|
|
18
|
+
'^.+\\.(t|j)sx?$': [
|
|
19
|
+
'<rootDir>/../../scripts/jest-mikroorm-transformer.cjs',
|
|
20
|
+
{
|
|
21
|
+
tsconfig: {
|
|
22
|
+
jsx: 'react-jsx',
|
|
23
|
+
rootDir: '.',
|
|
24
|
+
ignoreDeprecations: '6.0',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
transformIgnorePatterns: [
|
|
30
|
+
'node_modules/(?!(@mikro-orm|kysely)/)',
|
|
31
|
+
],
|
|
32
|
+
testMatch: ['<rootDir>/src/**/__tests__/**/*.test.(ts|tsx)'],
|
|
33
|
+
passWithNoTests: true,
|
|
34
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-mercato/channel-apns",
|
|
3
|
+
"version": "0.6.8-develop.6985.1.fb93574faa",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "node build.mjs",
|
|
9
|
+
"watch": "node watch.mjs",
|
|
10
|
+
"test": "jest --config jest.config.cjs",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./dist/index.js",
|
|
15
|
+
"./*.ts": {
|
|
16
|
+
"types": "./src/*.ts",
|
|
17
|
+
"default": "./dist/*.js"
|
|
18
|
+
},
|
|
19
|
+
"./*.tsx": {
|
|
20
|
+
"types": "./src/*.tsx",
|
|
21
|
+
"default": "./dist/*.js"
|
|
22
|
+
},
|
|
23
|
+
"./*.json": "./src/*.json",
|
|
24
|
+
"./*": {
|
|
25
|
+
"types": [
|
|
26
|
+
"./src/*.ts",
|
|
27
|
+
"./src/*.tsx"
|
|
28
|
+
],
|
|
29
|
+
"default": "./dist/*.js"
|
|
30
|
+
},
|
|
31
|
+
"./*/*.json": "./src/*/*.json",
|
|
32
|
+
"./*/*": {
|
|
33
|
+
"types": [
|
|
34
|
+
"./src/*/*.ts",
|
|
35
|
+
"./src/*/*.tsx"
|
|
36
|
+
],
|
|
37
|
+
"default": "./dist/*/*.js"
|
|
38
|
+
},
|
|
39
|
+
"./*/*/*.json": "./src/*/*/*.json",
|
|
40
|
+
"./*/*/*": {
|
|
41
|
+
"types": [
|
|
42
|
+
"./src/*/*/*.ts",
|
|
43
|
+
"./src/*/*/*.tsx"
|
|
44
|
+
],
|
|
45
|
+
"default": "./dist/*/*/*.js"
|
|
46
|
+
},
|
|
47
|
+
"./*/*/*/*.json": "./src/*/*/*/*.json",
|
|
48
|
+
"./*/*/*/*": {
|
|
49
|
+
"types": [
|
|
50
|
+
"./src/*/*/*/*.ts",
|
|
51
|
+
"./src/*/*/*/*.tsx"
|
|
52
|
+
],
|
|
53
|
+
"default": "./dist/*/*/*/*.js"
|
|
54
|
+
},
|
|
55
|
+
"./*/*/*/*/*.json": "./src/*/*/*/*/*.json",
|
|
56
|
+
"./*/*/*/*/*": {
|
|
57
|
+
"types": [
|
|
58
|
+
"./src/*/*/*/*/*.ts",
|
|
59
|
+
"./src/*/*/*/*/*.tsx"
|
|
60
|
+
],
|
|
61
|
+
"default": "./dist/*/*/*/*/*.js"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@open-mercato/core": "0.6.8-develop.6985.1.fb93574faa",
|
|
66
|
+
"@open-mercato/ui": "0.6.8-develop.6985.1.fb93574faa",
|
|
67
|
+
"@parse/node-apn": "^6.0.1"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"@mikro-orm/postgresql": "^7.0.14",
|
|
71
|
+
"@open-mercato/shared": "0.6.8-develop.6985.1.fb93574faa",
|
|
72
|
+
"react": "^19.0.0",
|
|
73
|
+
"react-dom": "^19.0.0"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@open-mercato/shared": "0.6.8-develop.6985.1.fb93574faa",
|
|
77
|
+
"@types/jest": "^30.0.0",
|
|
78
|
+
"@types/react": "^19.2.17",
|
|
79
|
+
"@types/react-dom": "^19.2.3",
|
|
80
|
+
"esbuild": "^0.28.1",
|
|
81
|
+
"glob": "^13.0.6",
|
|
82
|
+
"jest": "^30.4.2",
|
|
83
|
+
"react": "19.2.7",
|
|
84
|
+
"react-dom": "19.2.7",
|
|
85
|
+
"ts-jest": "^29.4.11"
|
|
86
|
+
},
|
|
87
|
+
"publishConfig": {
|
|
88
|
+
"access": "public"
|
|
89
|
+
},
|
|
90
|
+
"repository": {
|
|
91
|
+
"type": "git",
|
|
92
|
+
"url": "https://github.com/open-mercato/open-mercato",
|
|
93
|
+
"directory": "packages/channel-apns"
|
|
94
|
+
},
|
|
95
|
+
"stableVersion": "0.6.7"
|
|
96
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { metadata } from './modules/channel_apns/index'
|