@aglyn/aglyn 1.0.0-beta.263 → 1.0.0-beta.266
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/package.json +11 -11
- package/src/lib/app-utils/account-acquisition.d.ts +13 -0
- package/src/lib/app-utils/account-acquisition.js +84 -0
- package/src/lib/app-utils/account-acquisition.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +70 -0
- package/src/lib/app-utils/analytics-events.js +8 -0
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js +4 -1
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +2 -2
- package/src/lib/app-utils/docs-help.generated.js +8 -1
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +31 -10
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/expand-repeatables.js +14 -7
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/health-report.d.ts +47 -7
- package/src/lib/app-utils/health-report.js +31 -3
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/live-document-count.client.d.ts +28 -0
- package/src/lib/app-utils/live-document-count.client.js +35 -0
- package/src/lib/app-utils/live-document-count.client.js.map +1 -0
- package/src/lib/app-utils/live-document-count.d.ts +91 -0
- package/src/lib/app-utils/live-document-count.js +81 -0
- package/src/lib/app-utils/live-document-count.js.map +1 -0
- package/src/lib/app-utils/native-app-web-view.d.ts +63 -0
- package/src/lib/app-utils/native-app-web-view.js +83 -0
- package/src/lib/app-utils/native-app-web-view.js.map +1 -0
- package/src/lib/app-utils/notification-sounds.d.ts +49 -0
- package/src/lib/app-utils/notification-sounds.js +54 -0
- package/src/lib/app-utils/notification-sounds.js.map +1 -0
- package/src/lib/app-utils/notifications.d.ts +2 -0
- package/src/lib/app-utils/notifications.js +32 -0
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/screen-analytics-aggregate.d.ts +10 -0
- package/src/lib/app-utils/screen-analytics-aggregate.js +11 -0
- package/src/lib/app-utils/screen-analytics-aggregate.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +6 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +14 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js +30 -3
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type Query } from 'firebase/firestore';
|
|
18
|
+
/**
|
|
19
|
+
* The web SDK's half of the live-count rule (AGL-3918): how many of
|
|
20
|
+
* [documents] are not deleted, as `/api/hosts/resources` counts them for a
|
|
21
|
+
* plan allowance. Every document less the tombstones (`deletedAt != null`);
|
|
22
|
+
* an archived document is live and counted. See `live-document-count.ts` for
|
|
23
|
+
* why it is two aggregates.
|
|
24
|
+
*
|
|
25
|
+
* Rejects when either aggregate is refused, so a caller keeps whatever
|
|
26
|
+
* fallback it already has rather than reading a confident 0.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getLiveCountFromServer(documents: Query): Promise<number>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { getCountFromServer, query, where } from "firebase/firestore";
|
|
17
|
+
import { liveDocumentCount, TOMBSTONE_FIELD } from "./live-document-count.js";
|
|
18
|
+
/**
|
|
19
|
+
* The web SDK's half of the live-count rule (AGL-3918): how many of
|
|
20
|
+
* [documents] are not deleted, as `/api/hosts/resources` counts them for a
|
|
21
|
+
* plan allowance. Every document less the tombstones (`deletedAt != null`);
|
|
22
|
+
* an archived document is live and counted. See `live-document-count.ts` for
|
|
23
|
+
* why it is two aggregates.
|
|
24
|
+
*
|
|
25
|
+
* Rejects when either aggregate is refused, so a caller keeps whatever
|
|
26
|
+
* fallback it already has rather than reading a confident 0.
|
|
27
|
+
*/ export async function getLiveCountFromServer(documents) {
|
|
28
|
+
const [total, tombstones] = await Promise.all([
|
|
29
|
+
getCountFromServer(documents),
|
|
30
|
+
getCountFromServer(query(documents, where(TOMBSTONE_FIELD, '!=', null)))
|
|
31
|
+
]);
|
|
32
|
+
return liveDocumentCount(total.data().count, tombstones.data().count);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=live-document-count.client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/live-document-count.client.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { getCountFromServer, query, where, type Query } from 'firebase/firestore'\nimport { liveDocumentCount, TOMBSTONE_FIELD } from './live-document-count'\n\n/**\n * The web SDK's half of the live-count rule (AGL-3918): how many of\n * [documents] are not deleted, as `/api/hosts/resources` counts them for a\n * plan allowance. Every document less the tombstones (`deletedAt != null`);\n * an archived document is live and counted. See `live-document-count.ts` for\n * why it is two aggregates.\n *\n * Rejects when either aggregate is refused, so a caller keeps whatever\n * fallback it already has rather than reading a confident 0.\n */\nexport async function getLiveCountFromServer(documents: Query): Promise<number> {\n const [total, tombstones] = await Promise.all([\n getCountFromServer(documents),\n getCountFromServer(query(documents, where(TOMBSTONE_FIELD, '!=', null))),\n ])\n return liveDocumentCount(total.data().count, tombstones.data().count)\n}\n"],"names":["getCountFromServer","query","where","liveDocumentCount","TOMBSTONE_FIELD","getLiveCountFromServer","documents","total","tombstones","Promise","all","data","count"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,EAAEC,KAAK,EAAEC,KAAK,QAAoB,qBAAoB;AACjF,SAASC,iBAAiB,EAAEC,eAAe,QAAQ,2BAAuB;AAE1E;;;;;;;;;CASC,GACD,OAAO,eAAeC,uBAAuBC,SAAgB;IAC3D,MAAM,CAACC,OAAOC,WAAW,GAAG,MAAMC,QAAQC,GAAG,CAAC;QAC5CV,mBAAmBM;QACnBN,mBAAmBC,MAAMK,WAAWJ,MAAME,iBAAiB,MAAM;KAClE;IACD,OAAOD,kBAAkBI,MAAMI,IAAI,GAAGC,KAAK,EAAEJ,WAAWG,IAAI,GAAGC,KAAK;AACtE"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* How a plan allowance counts a collection whose delete is a SOFT delete
|
|
19
|
+
* (AGL-3918).
|
|
20
|
+
*
|
|
21
|
+
* The rule: a deleted document frees its slot, an archived one keeps it.
|
|
22
|
+
* Deleting stamps `deletedAt` and leaves the document in place (a tenant page
|
|
23
|
+
* keeps grafting until its next revalidate, a past order still names the
|
|
24
|
+
* product), so a plain `count()` charged a site for everything it had ever
|
|
25
|
+
* deleted: a product deleted on Starter never gave its slot back, and the
|
|
26
|
+
* Automation card read "4/25 workflows" beside a list of three. Archiving is a
|
|
27
|
+
* status the owner can reverse in place, so an archived product or a disabled
|
|
28
|
+
* workflow is still counted.
|
|
29
|
+
*
|
|
30
|
+
* ## Why total minus tombstones
|
|
31
|
+
*
|
|
32
|
+
* A live document has no `deletedAt` at all (created through
|
|
33
|
+
* `/api/hosts/resources`) or an explicit `deletedAt: null` (every product
|
|
34
|
+
* writer, a marketplace install). Firestore cannot match an absent field, so
|
|
35
|
+
* `where('deletedAt', '==', null)` counts the second shape alone. The
|
|
36
|
+
* TOMBSTONES are one shape — a value that is not `null` — and
|
|
37
|
+
* `where('deletedAt', '!=', null)` matches exactly them, so the total less
|
|
38
|
+
* that count is exactly the documents the server calls live. Two aggregates,
|
|
39
|
+
* one billed read each at these sizes, on the automatic single-field index
|
|
40
|
+
* `deletedAt` already has; no composite index.
|
|
41
|
+
*
|
|
42
|
+
* This module is SDK-agnostic: it names the Admin SDK's query shape
|
|
43
|
+
* structurally so a plugin, core and the console reach it without a second
|
|
44
|
+
* copy of the rule. The web SDK's counterpart is
|
|
45
|
+
* `live-document-count.client.ts`.
|
|
46
|
+
*/
|
|
47
|
+
/** The field every soft-deleting collection stamps on delete. */
|
|
48
|
+
export declare const TOMBSTONE_FIELD = "deletedAt";
|
|
49
|
+
/** The live count from the two aggregates, never below zero: they are separate reads, and a delete landing between them can make the second the larger. */
|
|
50
|
+
export declare function liveDocumentCount(total: number, tombstones: number): number;
|
|
51
|
+
/** Whether a stored document is live: no `deletedAt`, or `deletedAt: null`. */
|
|
52
|
+
export declare function isLiveDocument(deletedAt: unknown): boolean;
|
|
53
|
+
/** An aggregate query's snapshot, as the Admin SDK's `AggregateQuerySnapshot` answers it. */
|
|
54
|
+
export interface LiveCountSnapshot {
|
|
55
|
+
data(): {
|
|
56
|
+
count: number;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** An aggregate query the Admin SDK can execute directly. */
|
|
60
|
+
export interface LiveCountAggregate {
|
|
61
|
+
get(): Promise<LiveCountSnapshot>;
|
|
62
|
+
}
|
|
63
|
+
/** A collection or query the Admin SDK can count and filter. */
|
|
64
|
+
export interface LiveCountQuery<A extends LiveCountAggregate> {
|
|
65
|
+
count(): A;
|
|
66
|
+
where(field: string, op: '!=', value: null): {
|
|
67
|
+
count(): A;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The live documents [documents] matches: every one less the tombstones.
|
|
72
|
+
*
|
|
73
|
+
* `read` executes each aggregate, and defaults to executing it directly. A
|
|
74
|
+
* create that counts and writes in one transaction passes
|
|
75
|
+
* `(aggregate) => tx.get(aggregate)`, so both counts are the TRANSACTION's
|
|
76
|
+
* reads and the lock they take serializes a concurrent create (AGL-2231).
|
|
77
|
+
*/
|
|
78
|
+
export declare function countLiveDocuments<A extends LiveCountAggregate>(documents: LiveCountQuery<A>, read?: (aggregate: A) => Promise<LiveCountSnapshot>): Promise<number>;
|
|
79
|
+
/** The `source.type` a platform-seeded starter template carries (AGL-687). */
|
|
80
|
+
export declare const STARTER_TEMPLATE_SOURCE = "starter";
|
|
81
|
+
/**
|
|
82
|
+
* Of a site's deleted templates, how many `templatesPerHost`'s first count
|
|
83
|
+
* included: `where('source.type', '!=', 'starter')` matches a template that
|
|
84
|
+
* carries a `source.type` other than a starter's, and skips one without the
|
|
85
|
+
* field. Subtracting these from that count leaves the live templates the
|
|
86
|
+
* allowance counts, without the composite index a second inequality on
|
|
87
|
+
* `deletedAt` beside it would need.
|
|
88
|
+
*/
|
|
89
|
+
export declare function deletedTemplatesCounted(tombstones: ReadonlyArray<{
|
|
90
|
+
get(field: string): unknown;
|
|
91
|
+
}>): number;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* How a plan allowance counts a collection whose delete is a SOFT delete
|
|
18
|
+
* (AGL-3918).
|
|
19
|
+
*
|
|
20
|
+
* The rule: a deleted document frees its slot, an archived one keeps it.
|
|
21
|
+
* Deleting stamps `deletedAt` and leaves the document in place (a tenant page
|
|
22
|
+
* keeps grafting until its next revalidate, a past order still names the
|
|
23
|
+
* product), so a plain `count()` charged a site for everything it had ever
|
|
24
|
+
* deleted: a product deleted on Starter never gave its slot back, and the
|
|
25
|
+
* Automation card read "4/25 workflows" beside a list of three. Archiving is a
|
|
26
|
+
* status the owner can reverse in place, so an archived product or a disabled
|
|
27
|
+
* workflow is still counted.
|
|
28
|
+
*
|
|
29
|
+
* ## Why total minus tombstones
|
|
30
|
+
*
|
|
31
|
+
* A live document has no `deletedAt` at all (created through
|
|
32
|
+
* `/api/hosts/resources`) or an explicit `deletedAt: null` (every product
|
|
33
|
+
* writer, a marketplace install). Firestore cannot match an absent field, so
|
|
34
|
+
* `where('deletedAt', '==', null)` counts the second shape alone. The
|
|
35
|
+
* TOMBSTONES are one shape — a value that is not `null` — and
|
|
36
|
+
* `where('deletedAt', '!=', null)` matches exactly them, so the total less
|
|
37
|
+
* that count is exactly the documents the server calls live. Two aggregates,
|
|
38
|
+
* one billed read each at these sizes, on the automatic single-field index
|
|
39
|
+
* `deletedAt` already has; no composite index.
|
|
40
|
+
*
|
|
41
|
+
* This module is SDK-agnostic: it names the Admin SDK's query shape
|
|
42
|
+
* structurally so a plugin, core and the console reach it without a second
|
|
43
|
+
* copy of the rule. The web SDK's counterpart is
|
|
44
|
+
* `live-document-count.client.ts`.
|
|
45
|
+
*/ /** The field every soft-deleting collection stamps on delete. */ export const TOMBSTONE_FIELD = 'deletedAt';
|
|
46
|
+
/** The live count from the two aggregates, never below zero: they are separate reads, and a delete landing between them can make the second the larger. */ export function liveDocumentCount(total, tombstones) {
|
|
47
|
+
return Math.max(0, (Number(total) || 0) - (Number(tombstones) || 0));
|
|
48
|
+
}
|
|
49
|
+
/** Whether a stored document is live: no `deletedAt`, or `deletedAt: null`. */ export function isLiveDocument(deletedAt) {
|
|
50
|
+
return deletedAt == null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The live documents [documents] matches: every one less the tombstones.
|
|
54
|
+
*
|
|
55
|
+
* `read` executes each aggregate, and defaults to executing it directly. A
|
|
56
|
+
* create that counts and writes in one transaction passes
|
|
57
|
+
* `(aggregate) => tx.get(aggregate)`, so both counts are the TRANSACTION's
|
|
58
|
+
* reads and the lock they take serializes a concurrent create (AGL-2231).
|
|
59
|
+
*/ export async function countLiveDocuments(documents, read = (aggregate)=>aggregate.get()) {
|
|
60
|
+
const [total, tombstones] = await Promise.all([
|
|
61
|
+
read(documents.count()),
|
|
62
|
+
read(documents.where(TOMBSTONE_FIELD, '!=', null).count())
|
|
63
|
+
]);
|
|
64
|
+
return liveDocumentCount(total.data().count, tombstones.data().count);
|
|
65
|
+
}
|
|
66
|
+
/** The `source.type` a platform-seeded starter template carries (AGL-687). */ export const STARTER_TEMPLATE_SOURCE = 'starter';
|
|
67
|
+
/**
|
|
68
|
+
* Of a site's deleted templates, how many `templatesPerHost`'s first count
|
|
69
|
+
* included: `where('source.type', '!=', 'starter')` matches a template that
|
|
70
|
+
* carries a `source.type` other than a starter's, and skips one without the
|
|
71
|
+
* field. Subtracting these from that count leaves the live templates the
|
|
72
|
+
* allowance counts, without the composite index a second inequality on
|
|
73
|
+
* `deletedAt` beside it would need.
|
|
74
|
+
*/ export function deletedTemplatesCounted(tombstones) {
|
|
75
|
+
return tombstones.filter((entry)=>{
|
|
76
|
+
const type = entry.get('source.type');
|
|
77
|
+
return type != null && type !== STARTER_TEMPLATE_SOURCE;
|
|
78
|
+
}).length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=live-document-count.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/live-document-count.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * How a plan allowance counts a collection whose delete is a SOFT delete\n * (AGL-3918).\n *\n * The rule: a deleted document frees its slot, an archived one keeps it.\n * Deleting stamps `deletedAt` and leaves the document in place (a tenant page\n * keeps grafting until its next revalidate, a past order still names the\n * product), so a plain `count()` charged a site for everything it had ever\n * deleted: a product deleted on Starter never gave its slot back, and the\n * Automation card read \"4/25 workflows\" beside a list of three. Archiving is a\n * status the owner can reverse in place, so an archived product or a disabled\n * workflow is still counted.\n *\n * ## Why total minus tombstones\n *\n * A live document has no `deletedAt` at all (created through\n * `/api/hosts/resources`) or an explicit `deletedAt: null` (every product\n * writer, a marketplace install). Firestore cannot match an absent field, so\n * `where('deletedAt', '==', null)` counts the second shape alone. The\n * TOMBSTONES are one shape — a value that is not `null` — and\n * `where('deletedAt', '!=', null)` matches exactly them, so the total less\n * that count is exactly the documents the server calls live. Two aggregates,\n * one billed read each at these sizes, on the automatic single-field index\n * `deletedAt` already has; no composite index.\n *\n * This module is SDK-agnostic: it names the Admin SDK's query shape\n * structurally so a plugin, core and the console reach it without a second\n * copy of the rule. The web SDK's counterpart is\n * `live-document-count.client.ts`.\n */\n\n/** The field every soft-deleting collection stamps on delete. */\nexport const TOMBSTONE_FIELD = 'deletedAt'\n\n/** The live count from the two aggregates, never below zero: they are separate reads, and a delete landing between them can make the second the larger. */\nexport function liveDocumentCount(total: number, tombstones: number): number {\n return Math.max(0, (Number(total) || 0) - (Number(tombstones) || 0))\n}\n\n/** Whether a stored document is live: no `deletedAt`, or `deletedAt: null`. */\nexport function isLiveDocument(deletedAt: unknown): boolean {\n return deletedAt == null\n}\n\n/** An aggregate query's snapshot, as the Admin SDK's `AggregateQuerySnapshot` answers it. */\nexport interface LiveCountSnapshot {\n data(): { count: number }\n}\n\n/** An aggregate query the Admin SDK can execute directly. */\nexport interface LiveCountAggregate {\n get(): Promise<LiveCountSnapshot>\n}\n\n/** A collection or query the Admin SDK can count and filter. */\nexport interface LiveCountQuery<A extends LiveCountAggregate> {\n count(): A\n where(field: string, op: '!=', value: null): { count(): A }\n}\n\n/**\n * The live documents [documents] matches: every one less the tombstones.\n *\n * `read` executes each aggregate, and defaults to executing it directly. A\n * create that counts and writes in one transaction passes\n * `(aggregate) => tx.get(aggregate)`, so both counts are the TRANSACTION's\n * reads and the lock they take serializes a concurrent create (AGL-2231).\n */\nexport async function countLiveDocuments<A extends LiveCountAggregate>(\n documents: LiveCountQuery<A>,\n read: (aggregate: A) => Promise<LiveCountSnapshot> = (aggregate) => aggregate.get(),\n): Promise<number> {\n const [total, tombstones] = await Promise.all([\n read(documents.count()),\n read(documents.where(TOMBSTONE_FIELD, '!=', null).count()),\n ])\n return liveDocumentCount(total.data().count, tombstones.data().count)\n}\n\n/** The `source.type` a platform-seeded starter template carries (AGL-687). */\nexport const STARTER_TEMPLATE_SOURCE = 'starter'\n\n/**\n * Of a site's deleted templates, how many `templatesPerHost`'s first count\n * included: `where('source.type', '!=', 'starter')` matches a template that\n * carries a `source.type` other than a starter's, and skips one without the\n * field. Subtracting these from that count leaves the live templates the\n * allowance counts, without the composite index a second inequality on\n * `deletedAt` beside it would need.\n */\nexport function deletedTemplatesCounted(\n tombstones: ReadonlyArray<{ get(field: string): unknown }>,\n): number {\n return tombstones.filter((entry) => {\n const type = entry.get('source.type')\n return type != null && type !== STARTER_TEMPLATE_SOURCE\n }).length\n}\n"],"names":["TOMBSTONE_FIELD","liveDocumentCount","total","tombstones","Math","max","Number","isLiveDocument","deletedAt","countLiveDocuments","documents","read","aggregate","get","Promise","all","count","where","data","STARTER_TEMPLATE_SOURCE","deletedTemplatesCounted","filter","entry","type","length"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BC,GAED,+DAA+D,GAC/D,OAAO,MAAMA,kBAAkB,YAAW;AAE1C,yJAAyJ,GACzJ,OAAO,SAASC,kBAAkBC,KAAa,EAAEC,UAAkB;IACjE,OAAOC,KAAKC,GAAG,CAAC,GAAG,AAACC,CAAAA,OAAOJ,UAAU,CAAA,IAAMI,CAAAA,OAAOH,eAAe,CAAA;AACnE;AAEA,6EAA6E,GAC7E,OAAO,SAASI,eAAeC,SAAkB;IAC/C,OAAOA,aAAa;AACtB;AAkBA;;;;;;;CAOC,GACD,OAAO,eAAeC,mBACpBC,SAA4B,EAC5BC,OAAqD,CAACC,YAAcA,UAAUC,GAAG,EAAE;IAEnF,MAAM,CAACX,OAAOC,WAAW,GAAG,MAAMW,QAAQC,GAAG,CAAC;QAC5CJ,KAAKD,UAAUM,KAAK;QACpBL,KAAKD,UAAUO,KAAK,CAACjB,iBAAiB,MAAM,MAAMgB,KAAK;KACxD;IACD,OAAOf,kBAAkBC,MAAMgB,IAAI,GAAGF,KAAK,EAAEb,WAAWe,IAAI,GAAGF,KAAK;AACtE;AAEA,4EAA4E,GAC5E,OAAO,MAAMG,0BAA0B,UAAS;AAEhD;;;;;;;CAOC,GACD,OAAO,SAASC,wBACdjB,UAA0D;IAE1D,OAAOA,WAAWkB,MAAM,CAAC,CAACC;QACxB,MAAMC,OAAOD,MAAMT,GAAG,CAAC;QACvB,OAAOU,QAAQ,QAAQA,SAASJ;IAClC,GAAGK,MAAM;AACX"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Whether a page is running inside the native apps' web view (AGL-3894).
|
|
19
|
+
*
|
|
20
|
+
* The Aglyn apps (iPhone, iPad, Mac, Android, Windows) open the Besigner in
|
|
21
|
+
* an in-app web view. Inside it the app owns what a browser tab would offer:
|
|
22
|
+
* its own push notifications, its own Done button, its own window title. The
|
|
23
|
+
* page uses this to leave those web-only extras out.
|
|
24
|
+
*
|
|
25
|
+
* Two signals, either one enough:
|
|
26
|
+
*
|
|
27
|
+
* - **The user-agent marker.** Every app's web view appends
|
|
28
|
+
* `AglynApp/<version> (<platform>)` to its user agent (WKWebView's
|
|
29
|
+
* `applicationNameForUserAgent`, Android's `userAgentString`, WebView2's
|
|
30
|
+
* `UserAgent`). It is there from the first request, whichever page the
|
|
31
|
+
* view opens first, so it holds even for a build that predates the entry
|
|
32
|
+
* page below.
|
|
33
|
+
* - **The entry page's session mark.** `/auth/besigner` marks the tab when
|
|
34
|
+
* the app opens the Besigner through it (AGL-3850). Session storage lives
|
|
35
|
+
* exactly as long as the web view and never reaches a browser tab.
|
|
36
|
+
*
|
|
37
|
+
* PRESENTATION ONLY. A user agent is whatever the client says it is, so this
|
|
38
|
+
* decides what the page shows (a prompt, a back arrow, a title) and never
|
|
39
|
+
* what anyone may do: no auth, entitlement or security decision reads it.
|
|
40
|
+
*/
|
|
41
|
+
/** The product token every app's web view adds to its user agent. */
|
|
42
|
+
export declare const NATIVE_APP_USER_AGENT_PRODUCT = "AglynApp";
|
|
43
|
+
/**
|
|
44
|
+
* The marker the apps append, for the native code's specs and for anyone
|
|
45
|
+
* reading this file to know the exact shape: `AglynApp/1.4.0 (ios)`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function nativeAppUserAgentMarker(version: string, platform: string): string;
|
|
48
|
+
/** Whether a user-agent string carries the apps' marker. */
|
|
49
|
+
export declare function userAgentNamesNativeApp(userAgent: string | null | undefined): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Remember, for this tab, that it is the native apps' web view. Called by the
|
|
52
|
+
* Besigner entry page the apps open first.
|
|
53
|
+
*/
|
|
54
|
+
export declare function markNativeAppWebView(): void;
|
|
55
|
+
/** Whether this page is inside the native apps' web view. */
|
|
56
|
+
export declare function isNativeAppWebView(): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* `isNativeAppWebView()` for a render. The server cannot tell, so it renders
|
|
59
|
+
* the browser's page and hydration agrees with it; the client then renders
|
|
60
|
+
* again with the answer. Neither signal changes while a page is open, so
|
|
61
|
+
* there is nothing to subscribe to.
|
|
62
|
+
*/
|
|
63
|
+
export declare function useIsNativeAppWebView(): boolean;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { useSyncExternalStore } from "react";
|
|
17
|
+
/**
|
|
18
|
+
* Whether a page is running inside the native apps' web view (AGL-3894).
|
|
19
|
+
*
|
|
20
|
+
* The Aglyn apps (iPhone, iPad, Mac, Android, Windows) open the Besigner in
|
|
21
|
+
* an in-app web view. Inside it the app owns what a browser tab would offer:
|
|
22
|
+
* its own push notifications, its own Done button, its own window title. The
|
|
23
|
+
* page uses this to leave those web-only extras out.
|
|
24
|
+
*
|
|
25
|
+
* Two signals, either one enough:
|
|
26
|
+
*
|
|
27
|
+
* - **The user-agent marker.** Every app's web view appends
|
|
28
|
+
* `AglynApp/<version> (<platform>)` to its user agent (WKWebView's
|
|
29
|
+
* `applicationNameForUserAgent`, Android's `userAgentString`, WebView2's
|
|
30
|
+
* `UserAgent`). It is there from the first request, whichever page the
|
|
31
|
+
* view opens first, so it holds even for a build that predates the entry
|
|
32
|
+
* page below.
|
|
33
|
+
* - **The entry page's session mark.** `/auth/besigner` marks the tab when
|
|
34
|
+
* the app opens the Besigner through it (AGL-3850). Session storage lives
|
|
35
|
+
* exactly as long as the web view and never reaches a browser tab.
|
|
36
|
+
*
|
|
37
|
+
* PRESENTATION ONLY. A user agent is whatever the client says it is, so this
|
|
38
|
+
* decides what the page shows (a prompt, a back arrow, a title) and never
|
|
39
|
+
* what anyone may do: no auth, entitlement or security decision reads it.
|
|
40
|
+
*/ /** The product token every app's web view adds to its user agent. */ export const NATIVE_APP_USER_AGENT_PRODUCT = 'AglynApp';
|
|
41
|
+
/** Set by the Besigner entry page for the life of the web view's tab. */ const NATIVE_APP_WEB_VIEW_KEY = 'aglyn:native-app-web-view';
|
|
42
|
+
/** `AglynApp/<version>` as its own token: not inside a longer product name. */ const USER_AGENT_MARKER = new RegExp(`(?:^|[\\s(;])${NATIVE_APP_USER_AGENT_PRODUCT}/[^\\s/]+`);
|
|
43
|
+
/**
|
|
44
|
+
* The marker the apps append, for the native code's specs and for anyone
|
|
45
|
+
* reading this file to know the exact shape: `AglynApp/1.4.0 (ios)`.
|
|
46
|
+
*/ export function nativeAppUserAgentMarker(version, platform) {
|
|
47
|
+
return `${NATIVE_APP_USER_AGENT_PRODUCT}/${version || '0'} (${platform})`;
|
|
48
|
+
}
|
|
49
|
+
/** Whether a user-agent string carries the apps' marker. */ export function userAgentNamesNativeApp(userAgent) {
|
|
50
|
+
return typeof userAgent === 'string' && USER_AGENT_MARKER.test(userAgent);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Remember, for this tab, that it is the native apps' web view. Called by the
|
|
54
|
+
* Besigner entry page the apps open first.
|
|
55
|
+
*/ export function markNativeAppWebView() {
|
|
56
|
+
try {
|
|
57
|
+
window.sessionStorage.setItem(NATIVE_APP_WEB_VIEW_KEY, '1');
|
|
58
|
+
} catch (unused) {
|
|
59
|
+
// Storage refused: the user-agent marker still answers.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Whether this page is inside the native apps' web view. */ export function isNativeAppWebView() {
|
|
63
|
+
var _window_navigator;
|
|
64
|
+
if (typeof window === 'undefined') return false;
|
|
65
|
+
if (userAgentNamesNativeApp((_window_navigator = window.navigator) == null ? void 0 : _window_navigator.userAgent)) return true;
|
|
66
|
+
try {
|
|
67
|
+
return window.sessionStorage.getItem(NATIVE_APP_WEB_VIEW_KEY) === '1';
|
|
68
|
+
} catch (unused) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const subscribeNever = ()=>()=>undefined;
|
|
73
|
+
const onServer = ()=>false;
|
|
74
|
+
/**
|
|
75
|
+
* `isNativeAppWebView()` for a render. The server cannot tell, so it renders
|
|
76
|
+
* the browser's page and hydration agrees with it; the client then renders
|
|
77
|
+
* again with the answer. Neither signal changes while a page is open, so
|
|
78
|
+
* there is nothing to subscribe to.
|
|
79
|
+
*/ export function useIsNativeAppWebView() {
|
|
80
|
+
return useSyncExternalStore(subscribeNever, isNativeAppWebView, onServer);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=native-app-web-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/native-app-web-view.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { useSyncExternalStore } from 'react'\n\n/**\n * Whether a page is running inside the native apps' web view (AGL-3894).\n *\n * The Aglyn apps (iPhone, iPad, Mac, Android, Windows) open the Besigner in\n * an in-app web view. Inside it the app owns what a browser tab would offer:\n * its own push notifications, its own Done button, its own window title. The\n * page uses this to leave those web-only extras out.\n *\n * Two signals, either one enough:\n *\n * - **The user-agent marker.** Every app's web view appends\n * `AglynApp/<version> (<platform>)` to its user agent (WKWebView's\n * `applicationNameForUserAgent`, Android's `userAgentString`, WebView2's\n * `UserAgent`). It is there from the first request, whichever page the\n * view opens first, so it holds even for a build that predates the entry\n * page below.\n * - **The entry page's session mark.** `/auth/besigner` marks the tab when\n * the app opens the Besigner through it (AGL-3850). Session storage lives\n * exactly as long as the web view and never reaches a browser tab.\n *\n * PRESENTATION ONLY. A user agent is whatever the client says it is, so this\n * decides what the page shows (a prompt, a back arrow, a title) and never\n * what anyone may do: no auth, entitlement or security decision reads it.\n */\n\n/** The product token every app's web view adds to its user agent. */\nexport const NATIVE_APP_USER_AGENT_PRODUCT = 'AglynApp'\n\n/** Set by the Besigner entry page for the life of the web view's tab. */\nconst NATIVE_APP_WEB_VIEW_KEY = 'aglyn:native-app-web-view'\n\n/** `AglynApp/<version>` as its own token: not inside a longer product name. */\nconst USER_AGENT_MARKER = new RegExp(\n `(?:^|[\\\\s(;])${NATIVE_APP_USER_AGENT_PRODUCT}/[^\\\\s/]+`,\n)\n\n/**\n * The marker the apps append, for the native code's specs and for anyone\n * reading this file to know the exact shape: `AglynApp/1.4.0 (ios)`.\n */\nexport function nativeAppUserAgentMarker(\n version: string,\n platform: string,\n): string {\n return `${NATIVE_APP_USER_AGENT_PRODUCT}/${version || '0'} (${platform})`\n}\n\n/** Whether a user-agent string carries the apps' marker. */\nexport function userAgentNamesNativeApp(\n userAgent: string | null | undefined,\n): boolean {\n return typeof userAgent === 'string' && USER_AGENT_MARKER.test(userAgent)\n}\n\n/**\n * Remember, for this tab, that it is the native apps' web view. Called by the\n * Besigner entry page the apps open first.\n */\nexport function markNativeAppWebView(): void {\n try {\n window.sessionStorage.setItem(NATIVE_APP_WEB_VIEW_KEY, '1')\n } catch {\n // Storage refused: the user-agent marker still answers.\n }\n}\n\n/** Whether this page is inside the native apps' web view. */\nexport function isNativeAppWebView(): boolean {\n if (typeof window === 'undefined') return false\n if (userAgentNamesNativeApp(window.navigator?.userAgent)) return true\n try {\n return window.sessionStorage.getItem(NATIVE_APP_WEB_VIEW_KEY) === '1'\n } catch {\n return false\n }\n}\n\nconst subscribeNever = (): (() => void) => () => undefined\nconst onServer = (): boolean => false\n\n/**\n * `isNativeAppWebView()` for a render. The server cannot tell, so it renders\n * the browser's page and hydration agrees with it; the client then renders\n * again with the answer. Neither signal changes while a page is open, so\n * there is nothing to subscribe to.\n */\nexport function useIsNativeAppWebView(): boolean {\n return useSyncExternalStore(subscribeNever, isNativeAppWebView, onServer)\n}\n"],"names":["useSyncExternalStore","NATIVE_APP_USER_AGENT_PRODUCT","NATIVE_APP_WEB_VIEW_KEY","USER_AGENT_MARKER","RegExp","nativeAppUserAgentMarker","version","platform","userAgentNamesNativeApp","userAgent","test","markNativeAppWebView","window","sessionStorage","setItem","isNativeAppWebView","navigator","getItem","subscribeNever","undefined","onServer","useIsNativeAppWebView"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,QAAQ,QAAO;AAE5C;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,mEAAmE,GACnE,OAAO,MAAMC,gCAAgC,WAAU;AAEvD,uEAAuE,GACvE,MAAMC,0BAA0B;AAEhC,6EAA6E,GAC7E,MAAMC,oBAAoB,IAAIC,OAC5B,CAAC,aAAa,EAAEH,8BAA8B,SAAS,CAAC;AAG1D;;;CAGC,GACD,OAAO,SAASI,yBACdC,OAAe,EACfC,QAAgB;IAEhB,OAAO,GAAGN,8BAA8B,CAAC,EAAEK,WAAW,IAAI,EAAE,EAAEC,SAAS,CAAC,CAAC;AAC3E;AAEA,0DAA0D,GAC1D,OAAO,SAASC,wBACdC,SAAoC;IAEpC,OAAO,OAAOA,cAAc,YAAYN,kBAAkBO,IAAI,CAACD;AACjE;AAEA;;;CAGC,GACD,OAAO,SAASE;IACd,IAAI;QACFC,OAAOC,cAAc,CAACC,OAAO,CAACZ,yBAAyB;IACzD,EAAE,eAAM;IACN,wDAAwD;IAC1D;AACF;AAEA,2DAA2D,GAC3D,OAAO,SAASa;QAEcH;IAD5B,IAAI,OAAOA,WAAW,aAAa,OAAO;IAC1C,IAAIJ,yBAAwBI,oBAAAA,OAAOI,SAAS,qBAAhBJ,kBAAkBH,SAAS,GAAG,OAAO;IACjE,IAAI;QACF,OAAOG,OAAOC,cAAc,CAACI,OAAO,CAACf,6BAA6B;IACpE,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA,MAAMgB,iBAAiB,IAAoB,IAAMC;AACjD,MAAMC,WAAW,IAAe;AAEhC;;;;;CAKC,GACD,OAAO,SAASC;IACd,OAAOrB,qBAAqBkB,gBAAgBH,oBAAoBK;AAClE"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The notification sounds a person picks from (Notification settings), the
|
|
19
|
+
* same on every surface: the console's chime, the APNs `sound` the Apple apps
|
|
20
|
+
* play and the Android notification channel.
|
|
21
|
+
*
|
|
22
|
+
* NO IMPORTS, on purpose: the native apps read this list through the
|
|
23
|
+
* generated contracts, so it must stay a plain module.
|
|
24
|
+
*
|
|
25
|
+
* The audio is synthesized by `tools/notification-sounds/generate.py`, which
|
|
26
|
+
* writes each sound to the three paths named here; the two lists name the
|
|
27
|
+
* same ids.
|
|
28
|
+
*/
|
|
29
|
+
/** `users/{uid}.notificationSound`: the id of the sound the person picked. */
|
|
30
|
+
export declare const NOTIFICATION_SOUND_FIELD = "notificationSound";
|
|
31
|
+
export interface NotificationSound {
|
|
32
|
+
id: string;
|
|
33
|
+
label: string;
|
|
34
|
+
/** The console's file, served from `apps/console/public`. */
|
|
35
|
+
web: string;
|
|
36
|
+
/** The file in both Apple apps' bundles, named in the APNs payload's `sound`. */
|
|
37
|
+
apple: string;
|
|
38
|
+
/**
|
|
39
|
+
* The Android raw resource and the notification channel that plays it. A
|
|
40
|
+
* channel's sound cannot change once created, so each sound has its own.
|
|
41
|
+
*/
|
|
42
|
+
android: string;
|
|
43
|
+
}
|
|
44
|
+
/** In the order the picker lists them; the first is the default. */
|
|
45
|
+
export declare const NOTIFICATION_SOUNDS: readonly NotificationSound[];
|
|
46
|
+
/** What a person hears before they pick one. */
|
|
47
|
+
export declare const DEFAULT_NOTIFICATION_SOUND = "xylophone";
|
|
48
|
+
/** The stored pick as a sound, the default for no pick or an id no longer listed. */
|
|
49
|
+
export declare function notificationSound(value: unknown): NotificationSound;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* The notification sounds a person picks from (Notification settings), the
|
|
18
|
+
* same on every surface: the console's chime, the APNs `sound` the Apple apps
|
|
19
|
+
* play and the Android notification channel.
|
|
20
|
+
*
|
|
21
|
+
* NO IMPORTS, on purpose: the native apps read this list through the
|
|
22
|
+
* generated contracts, so it must stay a plain module.
|
|
23
|
+
*
|
|
24
|
+
* The audio is synthesized by `tools/notification-sounds/generate.py`, which
|
|
25
|
+
* writes each sound to the three paths named here; the two lists name the
|
|
26
|
+
* same ids.
|
|
27
|
+
*/ /** `users/{uid}.notificationSound`: the id of the sound the person picked. */ export const NOTIFICATION_SOUND_FIELD = 'notificationSound';
|
|
28
|
+
function sound(id, label) {
|
|
29
|
+
return {
|
|
30
|
+
id,
|
|
31
|
+
label,
|
|
32
|
+
web: `/sounds/notifications/${id}.wav`,
|
|
33
|
+
apple: `aglyn-${id}.caf`,
|
|
34
|
+
android: `aglyn_sound_${id}`
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** In the order the picker lists them; the first is the default. */ export const NOTIFICATION_SOUNDS = [
|
|
38
|
+
sound('xylophone', 'Xylophone'),
|
|
39
|
+
sound('signature', 'Signature'),
|
|
40
|
+
sound('tada', 'Ta-da'),
|
|
41
|
+
sound('plink', 'Plink'),
|
|
42
|
+
sound('arcade', 'Arcade'),
|
|
43
|
+
sound('cowbell', 'Cowbell'),
|
|
44
|
+
sound('bubbles', 'Bubbles'),
|
|
45
|
+
sound('steel_pan', 'Steel pan'),
|
|
46
|
+
sound('cash_register', 'Cash register')
|
|
47
|
+
];
|
|
48
|
+
/** What a person hears before they pick one. */ export const DEFAULT_NOTIFICATION_SOUND = 'xylophone';
|
|
49
|
+
/** The stored pick as a sound, the default for no pick or an id no longer listed. */ export function notificationSound(value) {
|
|
50
|
+
var _NOTIFICATION_SOUNDS_find;
|
|
51
|
+
return (_NOTIFICATION_SOUNDS_find = NOTIFICATION_SOUNDS.find((entry)=>entry.id === value)) != null ? _NOTIFICATION_SOUNDS_find : NOTIFICATION_SOUNDS.find((entry)=>entry.id === DEFAULT_NOTIFICATION_SOUND);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=notification-sounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/notification-sounds.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The notification sounds a person picks from (Notification settings), the\n * same on every surface: the console's chime, the APNs `sound` the Apple apps\n * play and the Android notification channel.\n *\n * NO IMPORTS, on purpose: the native apps read this list through the\n * generated contracts, so it must stay a plain module.\n *\n * The audio is synthesized by `tools/notification-sounds/generate.py`, which\n * writes each sound to the three paths named here; the two lists name the\n * same ids.\n */\n\n/** `users/{uid}.notificationSound`: the id of the sound the person picked. */\nexport const NOTIFICATION_SOUND_FIELD = 'notificationSound'\n\nexport interface NotificationSound {\n id: string\n label: string\n /** The console's file, served from `apps/console/public`. */\n web: string\n /** The file in both Apple apps' bundles, named in the APNs payload's `sound`. */\n apple: string\n /**\n * The Android raw resource and the notification channel that plays it. A\n * channel's sound cannot change once created, so each sound has its own.\n */\n android: string\n}\n\nfunction sound(id: string, label: string): NotificationSound {\n return {\n id,\n label,\n web: `/sounds/notifications/${id}.wav`,\n apple: `aglyn-${id}.caf`,\n android: `aglyn_sound_${id}`,\n }\n}\n\n/** In the order the picker lists them; the first is the default. */\nexport const NOTIFICATION_SOUNDS: readonly NotificationSound[] = [\n sound('xylophone', 'Xylophone'),\n sound('signature', 'Signature'),\n sound('tada', 'Ta-da'),\n sound('plink', 'Plink'),\n sound('arcade', 'Arcade'),\n sound('cowbell', 'Cowbell'),\n sound('bubbles', 'Bubbles'),\n sound('steel_pan', 'Steel pan'),\n sound('cash_register', 'Cash register'),\n]\n\n/** What a person hears before they pick one. */\nexport const DEFAULT_NOTIFICATION_SOUND = 'xylophone'\n\n/** The stored pick as a sound, the default for no pick or an id no longer listed. */\nexport function notificationSound(value: unknown): NotificationSound {\n return (\n NOTIFICATION_SOUNDS.find((entry) => entry.id === value) ??\n (NOTIFICATION_SOUNDS.find((entry) => entry.id === DEFAULT_NOTIFICATION_SOUND) as NotificationSound)\n )\n}\n"],"names":["NOTIFICATION_SOUND_FIELD","sound","id","label","web","apple","android","NOTIFICATION_SOUNDS","DEFAULT_NOTIFICATION_SOUND","notificationSound","value","find","entry"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GAED,4EAA4E,GAC5E,OAAO,MAAMA,2BAA2B,oBAAmB;AAgB3D,SAASC,MAAMC,EAAU,EAAEC,KAAa;IACtC,OAAO;QACLD;QACAC;QACAC,KAAK,CAAC,sBAAsB,EAAEF,GAAG,IAAI,CAAC;QACtCG,OAAO,CAAC,MAAM,EAAEH,GAAG,IAAI,CAAC;QACxBI,SAAS,CAAC,YAAY,EAAEJ,IAAI;IAC9B;AACF;AAEA,kEAAkE,GAClE,OAAO,MAAMK,sBAAoD;IAC/DN,MAAM,aAAa;IACnBA,MAAM,aAAa;IACnBA,MAAM,QAAQ;IACdA,MAAM,SAAS;IACfA,MAAM,UAAU;IAChBA,MAAM,WAAW;IACjBA,MAAM,WAAW;IACjBA,MAAM,aAAa;IACnBA,MAAM,iBAAiB;CACxB,CAAA;AAED,8CAA8C,GAC9C,OAAO,MAAMO,6BAA6B,YAAW;AAErD,mFAAmF,GACnF,OAAO,SAASC,kBAAkBC,KAAc;QAE5CH;IADF,QACEA,4BAAAA,oBAAoBI,IAAI,CAAC,CAACC,QAAUA,MAAMV,EAAE,KAAKQ,kBAAjDH,4BACCA,oBAAoBI,IAAI,CAAC,CAACC,QAAUA,MAAMV,EAAE,KAAKM;AAEtD"}
|
|
@@ -349,6 +349,8 @@ export declare const NOTIFICATION_CHANNEL_DEFAULTS: Readonly<Record<Notification
|
|
|
349
349
|
* see {@link NOTIFICATION_SELF_SENT_EMAIL_TYPES} and the `skipEmail` callers.
|
|
350
350
|
*/
|
|
351
351
|
export declare const NOTIFICATION_TYPE_CHANNEL_DEFAULTS: Partial<Record<AglynNotificationType, Partial<Record<NotificationChannel, boolean>>>>;
|
|
352
|
+
/** The words on a notification email's button: the record it opens, else "Open in {product}". */
|
|
353
|
+
export declare function notificationEmailAction(type: AglynNotificationType | string, productName: string): string;
|
|
352
354
|
/** What a type does on a channel when nobody has answered for it or its category. */
|
|
353
355
|
export declare function notificationTypeChannelDefault(type: AglynNotificationType | string, channel: NotificationChannel): boolean;
|
|
354
356
|
/**
|
|
@@ -443,6 +443,38 @@ export const NOTIFICATION_CHANNEL_DEFAULTS = inCategoryOrder(CORE_CHANNEL_DEFAUL
|
|
|
443
443
|
email: true
|
|
444
444
|
}
|
|
445
445
|
};
|
|
446
|
+
/**
|
|
447
|
+
* What a notification email's button says it opens (AGL-3897), by type: the
|
|
448
|
+
* record the notification links to, named, rather than "Open in Aglyn" on
|
|
449
|
+
* every one. A type with no entry keeps that general label.
|
|
450
|
+
*/ const NOTIFICATION_EMAIL_ACTIONS = {
|
|
451
|
+
'content.formSubmission': 'View submission',
|
|
452
|
+
'content.booking': 'View booking',
|
|
453
|
+
'content.order': 'View order',
|
|
454
|
+
'content.lowStock': 'View product',
|
|
455
|
+
'content.taskAssigned': 'Open task',
|
|
456
|
+
'content.taskReminder': 'Open task',
|
|
457
|
+
'content.contactAssigned': 'Open contact',
|
|
458
|
+
'content.leadAssigned': 'Open lead',
|
|
459
|
+
'billing.invoice': 'Open billing',
|
|
460
|
+
'billing.paymentFailed': 'Update payment method',
|
|
461
|
+
'billing.subscriptionCanceled': 'Open billing',
|
|
462
|
+
'billing.usage': 'Open billing',
|
|
463
|
+
'support.ticketOpened': 'Open ticket',
|
|
464
|
+
'support.ticketReply': 'Open ticket',
|
|
465
|
+
'marketplace.review': 'Open listing',
|
|
466
|
+
'staff.userSignedUp': 'Open account',
|
|
467
|
+
'staff.orgCreated': 'Open workspace',
|
|
468
|
+
'staff.siteCreated': 'Open site',
|
|
469
|
+
'staff.subscriptionStarted': 'Open workspace',
|
|
470
|
+
'staff.subscriptionCanceled': 'Open workspace',
|
|
471
|
+
'staff.planChanged': 'Open workspace',
|
|
472
|
+
'staff.paymentFailed': 'Open workspace'
|
|
473
|
+
};
|
|
474
|
+
/** The words on a notification email's button: the record it opens, else "Open in {product}". */ export function notificationEmailAction(type, productName) {
|
|
475
|
+
var _NOTIFICATION_EMAIL_ACTIONS_type;
|
|
476
|
+
return (_NOTIFICATION_EMAIL_ACTIONS_type = NOTIFICATION_EMAIL_ACTIONS[type]) != null ? _NOTIFICATION_EMAIL_ACTIONS_type : `Open in ${productName}`;
|
|
477
|
+
}
|
|
446
478
|
/** What a type does on a channel when nobody has answered for it or its category. */ export function notificationTypeChannelDefault(type, channel) {
|
|
447
479
|
var _NOTIFICATION_TYPE_CHANNEL_DEFAULTS_type;
|
|
448
480
|
const own = (_NOTIFICATION_TYPE_CHANNEL_DEFAULTS_type = NOTIFICATION_TYPE_CHANNEL_DEFAULTS[type]) == null ? void 0 : _NOTIFICATION_TYPE_CHANNEL_DEFAULTS_type[channel];
|