@stardeck-customer-apps/controls 0.3.0 → 0.5.0
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/SKILL.md +80 -6
- package/dist/index.cjs +44 -0
- package/dist/index.d.cts +50 -1
- package/dist/index.d.ts +50 -1
- package/dist/index.js +43 -0
- package/dist/server/index.cjs +42 -0
- package/dist/server/index.d.cts +31 -0
- package/dist/server/index.d.ts +31 -0
- package/dist/server/index.js +42 -0
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
Controls are values Stardeck owns and your app reads while it runs: feature
|
|
6
6
|
flags, what the plan allows, how many locations have POS Pro hardware, which
|
|
7
|
-
Accounting edition the business pays for,
|
|
8
|
-
the app uses
|
|
7
|
+
Accounting edition the business pays for, which payment provider and methods
|
|
8
|
+
the app uses, and who the business is (its name, address, tax ID, currency and
|
|
9
|
+
time zone). Your app asks Stardeck about once a minute and keeps the answer in memory.
|
|
9
10
|
A change on Stardeck reaches the app within about a minute, with no deploy.
|
|
10
11
|
|
|
11
12
|
Never invent controls or flags of your own. The list is fixed by Stardeck and
|
|
@@ -53,6 +54,8 @@ the last values stay.
|
|
|
53
54
|
| `plan.accountingEdition` | `"small" \| "medium" \| null` | no |
|
|
54
55
|
| `payments.provider` | `"beam" \| "stripe" \| null` | yes |
|
|
55
56
|
| `payments.methods` | `{ card: boolean, promptPay: boolean }` | yes |
|
|
57
|
+
| `business.profile` | `BusinessProfile \| null` (see "Business details") | yes |
|
|
58
|
+
| `business.logo` | `{ url: string } \| null` (see "Business logo") | no |
|
|
56
59
|
|
|
57
60
|
- `plan.locations.included` is what the plan covers. `max` is a hard cap (the
|
|
58
61
|
entry plan has 1); `null` means no cap, and each location past `included` is
|
|
@@ -63,6 +66,11 @@ the last values stay.
|
|
|
63
66
|
- `plan.accountingEdition` is the Accounting edition the business pays for, an
|
|
64
67
|
add-on next to its plan. `null` means it has no Accounting plan. See
|
|
65
68
|
"Accounting edition" below.
|
|
69
|
+
- `business.profile` is the business the app is for, as Stardeck keeps it for
|
|
70
|
+
the organization. `null` means Stardeck has no record yet. See "Business
|
|
71
|
+
details" below.
|
|
72
|
+
- `business.logo` is the organization's logo (controls 0.5.0 or later). `null`
|
|
73
|
+
means it has none. See "Business logo" below.
|
|
66
74
|
- Keys are never removed in later versions, so code written against today's list
|
|
67
75
|
keeps working.
|
|
68
76
|
|
|
@@ -116,6 +124,71 @@ if (edition.value === null) {
|
|
|
116
124
|
// edition.value is "small" or "medium"
|
|
117
125
|
```
|
|
118
126
|
|
|
127
|
+
## Business details
|
|
128
|
+
|
|
129
|
+
Stardeck keeps one business record per organization: what the owner told
|
|
130
|
+
Stardeck when they signed up, edited in Stardeck's Business settings. Every app
|
|
131
|
+
in the organization reads the same record (needs controls 0.4.0 or later):
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
type BusinessProfile = {
|
|
135
|
+
kind: string; // a business preset id: "restaurant", "spa", "general"…
|
|
136
|
+
name: { en: string; th: string | null };
|
|
137
|
+
headOfficeAddress: string | null;
|
|
138
|
+
taxId: string | null;
|
|
139
|
+
country: string; // ISO 3166-1 alpha-2, "TH"
|
|
140
|
+
currency: string; // ISO 4217, "THB"
|
|
141
|
+
timezone: string; // IANA, "Asia/Bangkok"
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Stardeck owns it; the app keeps a copy. Mirror it into the app's own records
|
|
146
|
+
(the default company, the currency and time zone settings, the business preset)
|
|
147
|
+
whenever the value differs from what the app holds:
|
|
148
|
+
|
|
149
|
+
- `{ ok: true, value }`: copy `value` into the app. A `kind` this app does not
|
|
150
|
+
have stays on the general preset; never fail on it.
|
|
151
|
+
- `{ ok: true, value: null }`: Stardeck has no record yet. If the app already
|
|
152
|
+
has its own company details, send them up once with `setControl`; otherwise
|
|
153
|
+
ask the owner as before.
|
|
154
|
+
- `ok: false`: unknown right now, which is not the same as `null`. Change
|
|
155
|
+
nothing and keep the company the app has. Never blank a name or address
|
|
156
|
+
because of `ok: false`.
|
|
157
|
+
|
|
158
|
+
When the owner edits the business inside the app, write it to Stardeck first
|
|
159
|
+
with `setControl("business.profile", profile, { userToken })`, then save the
|
|
160
|
+
app's own copy from the answer. Saving only the app's copy would be undone by
|
|
161
|
+
the next mirror. Only an admin of the organization on Stardeck may write it, so
|
|
162
|
+
a `FORBIDDEN` answer means "ask an organization admin"; show `error.message`.
|
|
163
|
+
`null` is refused (`INVALID_VALUE`): the app can change the details but not
|
|
164
|
+
remove them. Validate input with the same rules Stardeck uses:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { businessProfileSchema } from "@stardeck-customer-apps/controls";
|
|
168
|
+
|
|
169
|
+
const parsed = businessProfileSchema.safeParse(input);
|
|
170
|
+
if (!parsed.success) return badRequest(parsed.error.issues[0]?.message);
|
|
171
|
+
const result = await setControl("business.profile", parsed.data, { userToken });
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Business logo
|
|
175
|
+
|
|
176
|
+
The organization's logo is the icon it set on Stardeck (onboarding asks for
|
|
177
|
+
one). It is the default logo for every app in the organization (needs controls
|
|
178
|
+
0.5.0 or later). Show it wherever the app shows a logo and has none of its own:
|
|
179
|
+
the site header, the admin shell, emails, receipts. When the owner uploads a
|
|
180
|
+
logo in the app, that one wins for this app; never copy the organization's logo
|
|
181
|
+
into the app's own setting, or a later change on Stardeck won't reach it.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const orgLogo = await getControl("business.logo");
|
|
185
|
+
// ok: false is "unknown right now": show the app's own logo, or none, and never fail.
|
|
186
|
+
const logoUrl = appLogoUrl ?? (orgLogo.ok ? (orgLogo.value?.url ?? null) : null);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The URL is an https image Stardeck's sign-in provider hosts. It is read-only:
|
|
190
|
+
`setControl("business.logo", …)` is refused with `NOT_WRITABLE`.
|
|
191
|
+
|
|
119
192
|
Types and the list itself are in the root export:
|
|
120
193
|
|
|
121
194
|
```ts
|
|
@@ -124,10 +197,11 @@ import { CONTROLS, parseControls, type ControlKey } from "@stardeck-customer-app
|
|
|
124
197
|
|
|
125
198
|
## Changing provider or methods
|
|
126
199
|
|
|
127
|
-
Only `payments.provider` and `
|
|
128
|
-
only on behalf of a signed-in person. Pass that
|
|
129
|
-
checks that this person may change the app's
|
|
130
|
-
|
|
200
|
+
Only `payments.provider`, `payments.methods` and `business.profile` can be
|
|
201
|
+
changed from the app, and only on behalf of a signed-in person. Pass that
|
|
202
|
+
person's session token. Stardeck checks that this person may change the app's
|
|
203
|
+
settings (for `business.profile`, that they are an admin of the organization);
|
|
204
|
+
the app's own roles are never enough.
|
|
131
205
|
|
|
132
206
|
```ts
|
|
133
207
|
import { cookies } from "next/headers";
|
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
CONTROLS: () => CONTROLS,
|
|
24
24
|
CONTROL_KEYS: () => CONTROL_KEYS,
|
|
25
|
+
businessProfileSchema: () => businessProfileSchema,
|
|
25
26
|
isControlKey: () => isControlKey,
|
|
26
27
|
isWritableControl: () => isWritableControl,
|
|
27
28
|
parseControls: () => parseControls,
|
|
@@ -31,6 +32,33 @@ module.exports = __toCommonJS(index_exports);
|
|
|
31
32
|
|
|
32
33
|
// src/controls.ts
|
|
33
34
|
var import_zod = require("zod");
|
|
35
|
+
function isIanaTimeZone(value) {
|
|
36
|
+
try {
|
|
37
|
+
new Intl.DateTimeFormat("en", { timeZone: value });
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
var businessProfileSchema = import_zod.z.object({
|
|
44
|
+
/** The kind of business: a business preset id the app defines (`restaurant`, `spa`, `general`…). */
|
|
45
|
+
kind: import_zod.z.string().regex(/^[a-z][a-z0-9-]{0,39}$/, "a business kind is a lowercase preset id"),
|
|
46
|
+
/** The company's name. Thai is null when the business has no Thai name. */
|
|
47
|
+
name: import_zod.z.object({
|
|
48
|
+
en: import_zod.z.string().trim().min(1, "the business needs a name").max(200),
|
|
49
|
+
th: import_zod.z.string().trim().min(1).max(200).nullable()
|
|
50
|
+
}),
|
|
51
|
+
/** Head-office address as one block of text; null until someone enters it. */
|
|
52
|
+
headOfficeAddress: import_zod.z.string().trim().min(1).max(1e3).nullable(),
|
|
53
|
+
/** Tax ID; null for a business with none (under the VAT threshold, say). */
|
|
54
|
+
taxId: import_zod.z.string().trim().min(1).max(40).nullable(),
|
|
55
|
+
/** ISO 3166-1 alpha-2. */
|
|
56
|
+
country: import_zod.z.string().regex(/^[A-Z]{2}$/, "country is an ISO 3166-1 alpha-2 code"),
|
|
57
|
+
/** ISO 4217. */
|
|
58
|
+
currency: import_zod.z.string().regex(/^[A-Z]{3}$/, "currency is an ISO 4217 code"),
|
|
59
|
+
/** IANA time zone. */
|
|
60
|
+
timezone: import_zod.z.string().refine(isIanaTimeZone, "timezone is not an IANA time zone")
|
|
61
|
+
});
|
|
34
62
|
var CONTROLS = {
|
|
35
63
|
"flag.paymentsSetupStep": { schema: import_zod.z.boolean(), appWrite: false },
|
|
36
64
|
"plan.locations": {
|
|
@@ -68,6 +96,21 @@ var CONTROLS = {
|
|
|
68
96
|
"plan.accountingEdition": {
|
|
69
97
|
schema: import_zod.z.enum(["small", "medium"]).nullable(),
|
|
70
98
|
appWrite: false
|
|
99
|
+
},
|
|
100
|
+
/**
|
|
101
|
+
* The business this app is for, owned by Stardeck. null means Stardeck has no record yet: an
|
|
102
|
+
* app that already has its own company details sends them up once. `{ ok: false }` means
|
|
103
|
+
* unknown right now: keep the company the app has.
|
|
104
|
+
*/
|
|
105
|
+
"business.profile": { schema: businessProfileSchema.nullable(), appWrite: true },
|
|
106
|
+
/**
|
|
107
|
+
* The org's logo: the icon it set in Stardeck (onboarding asks for one). The app shows it
|
|
108
|
+
* wherever it has no logo of its own, and an owner's own upload always wins. null means the
|
|
109
|
+
* org has no logo. Read-only: the app changes its own logo, never the org's.
|
|
110
|
+
*/
|
|
111
|
+
"business.logo": {
|
|
112
|
+
schema: import_zod.z.object({ url: import_zod.z.string().url().startsWith("https://", "the logo is served over https") }).nullable(),
|
|
113
|
+
appWrite: false
|
|
71
114
|
}
|
|
72
115
|
};
|
|
73
116
|
var CONTROL_KEYS = Object.keys(CONTROLS);
|
|
@@ -114,6 +157,7 @@ function unavailableControls(reason) {
|
|
|
114
157
|
0 && (module.exports = {
|
|
115
158
|
CONTROLS,
|
|
116
159
|
CONTROL_KEYS,
|
|
160
|
+
businessProfileSchema,
|
|
117
161
|
isControlKey,
|
|
118
162
|
isWritableControl,
|
|
119
163
|
parseControls,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Who the business is, as Stardeck keeps it for the org: what onboarding asks and Stardeck's
|
|
5
|
+
* Business settings edit. The app mirrors it into its default company and workspace settings, and
|
|
6
|
+
* sends an owner's edit back through this same control, so Stardeck stays the one source.
|
|
7
|
+
*/
|
|
8
|
+
declare const businessProfileSchema: z.ZodObject<{
|
|
9
|
+
kind: z.ZodString;
|
|
10
|
+
name: z.ZodObject<{
|
|
11
|
+
en: z.ZodString;
|
|
12
|
+
th: z.ZodNullable<z.ZodString>;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
headOfficeAddress: z.ZodNullable<z.ZodString>;
|
|
15
|
+
taxId: z.ZodNullable<z.ZodString>;
|
|
16
|
+
country: z.ZodString;
|
|
17
|
+
currency: z.ZodString;
|
|
18
|
+
timezone: z.ZodString;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
type BusinessProfile = z.infer<typeof businessProfileSchema>;
|
|
3
21
|
/**
|
|
4
22
|
* The controls list: every value the platform owns and an app reads.
|
|
5
23
|
*
|
|
@@ -66,6 +84,37 @@ declare const CONTROLS: {
|
|
|
66
84
|
}>>;
|
|
67
85
|
readonly appWrite: false;
|
|
68
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* The business this app is for, owned by Stardeck. null means Stardeck has no record yet: an
|
|
89
|
+
* app that already has its own company details sends them up once. `{ ok: false }` means
|
|
90
|
+
* unknown right now: keep the company the app has.
|
|
91
|
+
*/
|
|
92
|
+
readonly "business.profile": {
|
|
93
|
+
readonly schema: z.ZodNullable<z.ZodObject<{
|
|
94
|
+
kind: z.ZodString;
|
|
95
|
+
name: z.ZodObject<{
|
|
96
|
+
en: z.ZodString;
|
|
97
|
+
th: z.ZodNullable<z.ZodString>;
|
|
98
|
+
}, z.core.$strip>;
|
|
99
|
+
headOfficeAddress: z.ZodNullable<z.ZodString>;
|
|
100
|
+
taxId: z.ZodNullable<z.ZodString>;
|
|
101
|
+
country: z.ZodString;
|
|
102
|
+
currency: z.ZodString;
|
|
103
|
+
timezone: z.ZodString;
|
|
104
|
+
}, z.core.$strip>>;
|
|
105
|
+
readonly appWrite: true;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* The org's logo: the icon it set in Stardeck (onboarding asks for one). The app shows it
|
|
109
|
+
* wherever it has no logo of its own, and an owner's own upload always wins. null means the
|
|
110
|
+
* org has no logo. Read-only: the app changes its own logo, never the org's.
|
|
111
|
+
*/
|
|
112
|
+
readonly "business.logo": {
|
|
113
|
+
readonly schema: z.ZodNullable<z.ZodObject<{
|
|
114
|
+
url: z.ZodString;
|
|
115
|
+
}, z.core.$strip>>;
|
|
116
|
+
readonly appWrite: false;
|
|
117
|
+
};
|
|
69
118
|
};
|
|
70
119
|
type ControlKey = keyof typeof CONTROLS;
|
|
71
120
|
type ControlValue<K extends ControlKey> = z.infer<(typeof CONTROLS)[K]["schema"]>;
|
|
@@ -118,4 +167,4 @@ declare function parseControls(raw: unknown): Controls;
|
|
|
118
167
|
/** Every control unavailable for one reason (cold start with the platform down, or not configured). */
|
|
119
168
|
declare function unavailableControls(reason: string): Controls;
|
|
120
169
|
|
|
121
|
-
export { CONTROLS, CONTROL_KEYS, type ControlKey, type ControlResult, type ControlValue, type ControlWriteError, type ControlWriteErrorCode, type ControlWriteRequest, type Controls, type ControlsSyncRequest, type ControlsSyncResponse, type ControlsUsage, type WritableControlKey, isControlKey, isWritableControl, parseControls, unavailableControls };
|
|
170
|
+
export { type BusinessProfile, CONTROLS, CONTROL_KEYS, type ControlKey, type ControlResult, type ControlValue, type ControlWriteError, type ControlWriteErrorCode, type ControlWriteRequest, type Controls, type ControlsSyncRequest, type ControlsSyncResponse, type ControlsUsage, type WritableControlKey, businessProfileSchema, isControlKey, isWritableControl, parseControls, unavailableControls };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Who the business is, as Stardeck keeps it for the org: what onboarding asks and Stardeck's
|
|
5
|
+
* Business settings edit. The app mirrors it into its default company and workspace settings, and
|
|
6
|
+
* sends an owner's edit back through this same control, so Stardeck stays the one source.
|
|
7
|
+
*/
|
|
8
|
+
declare const businessProfileSchema: z.ZodObject<{
|
|
9
|
+
kind: z.ZodString;
|
|
10
|
+
name: z.ZodObject<{
|
|
11
|
+
en: z.ZodString;
|
|
12
|
+
th: z.ZodNullable<z.ZodString>;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
headOfficeAddress: z.ZodNullable<z.ZodString>;
|
|
15
|
+
taxId: z.ZodNullable<z.ZodString>;
|
|
16
|
+
country: z.ZodString;
|
|
17
|
+
currency: z.ZodString;
|
|
18
|
+
timezone: z.ZodString;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
type BusinessProfile = z.infer<typeof businessProfileSchema>;
|
|
3
21
|
/**
|
|
4
22
|
* The controls list: every value the platform owns and an app reads.
|
|
5
23
|
*
|
|
@@ -66,6 +84,37 @@ declare const CONTROLS: {
|
|
|
66
84
|
}>>;
|
|
67
85
|
readonly appWrite: false;
|
|
68
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* The business this app is for, owned by Stardeck. null means Stardeck has no record yet: an
|
|
89
|
+
* app that already has its own company details sends them up once. `{ ok: false }` means
|
|
90
|
+
* unknown right now: keep the company the app has.
|
|
91
|
+
*/
|
|
92
|
+
readonly "business.profile": {
|
|
93
|
+
readonly schema: z.ZodNullable<z.ZodObject<{
|
|
94
|
+
kind: z.ZodString;
|
|
95
|
+
name: z.ZodObject<{
|
|
96
|
+
en: z.ZodString;
|
|
97
|
+
th: z.ZodNullable<z.ZodString>;
|
|
98
|
+
}, z.core.$strip>;
|
|
99
|
+
headOfficeAddress: z.ZodNullable<z.ZodString>;
|
|
100
|
+
taxId: z.ZodNullable<z.ZodString>;
|
|
101
|
+
country: z.ZodString;
|
|
102
|
+
currency: z.ZodString;
|
|
103
|
+
timezone: z.ZodString;
|
|
104
|
+
}, z.core.$strip>>;
|
|
105
|
+
readonly appWrite: true;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* The org's logo: the icon it set in Stardeck (onboarding asks for one). The app shows it
|
|
109
|
+
* wherever it has no logo of its own, and an owner's own upload always wins. null means the
|
|
110
|
+
* org has no logo. Read-only: the app changes its own logo, never the org's.
|
|
111
|
+
*/
|
|
112
|
+
readonly "business.logo": {
|
|
113
|
+
readonly schema: z.ZodNullable<z.ZodObject<{
|
|
114
|
+
url: z.ZodString;
|
|
115
|
+
}, z.core.$strip>>;
|
|
116
|
+
readonly appWrite: false;
|
|
117
|
+
};
|
|
69
118
|
};
|
|
70
119
|
type ControlKey = keyof typeof CONTROLS;
|
|
71
120
|
type ControlValue<K extends ControlKey> = z.infer<(typeof CONTROLS)[K]["schema"]>;
|
|
@@ -118,4 +167,4 @@ declare function parseControls(raw: unknown): Controls;
|
|
|
118
167
|
/** Every control unavailable for one reason (cold start with the platform down, or not configured). */
|
|
119
168
|
declare function unavailableControls(reason: string): Controls;
|
|
120
169
|
|
|
121
|
-
export { CONTROLS, CONTROL_KEYS, type ControlKey, type ControlResult, type ControlValue, type ControlWriteError, type ControlWriteErrorCode, type ControlWriteRequest, type Controls, type ControlsSyncRequest, type ControlsSyncResponse, type ControlsUsage, type WritableControlKey, isControlKey, isWritableControl, parseControls, unavailableControls };
|
|
170
|
+
export { type BusinessProfile, CONTROLS, CONTROL_KEYS, type ControlKey, type ControlResult, type ControlValue, type ControlWriteError, type ControlWriteErrorCode, type ControlWriteRequest, type Controls, type ControlsSyncRequest, type ControlsSyncResponse, type ControlsUsage, type WritableControlKey, businessProfileSchema, isControlKey, isWritableControl, parseControls, unavailableControls };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
// src/controls.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
function isIanaTimeZone(value) {
|
|
4
|
+
try {
|
|
5
|
+
new Intl.DateTimeFormat("en", { timeZone: value });
|
|
6
|
+
return true;
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
var businessProfileSchema = z.object({
|
|
12
|
+
/** The kind of business: a business preset id the app defines (`restaurant`, `spa`, `general`…). */
|
|
13
|
+
kind: z.string().regex(/^[a-z][a-z0-9-]{0,39}$/, "a business kind is a lowercase preset id"),
|
|
14
|
+
/** The company's name. Thai is null when the business has no Thai name. */
|
|
15
|
+
name: z.object({
|
|
16
|
+
en: z.string().trim().min(1, "the business needs a name").max(200),
|
|
17
|
+
th: z.string().trim().min(1).max(200).nullable()
|
|
18
|
+
}),
|
|
19
|
+
/** Head-office address as one block of text; null until someone enters it. */
|
|
20
|
+
headOfficeAddress: z.string().trim().min(1).max(1e3).nullable(),
|
|
21
|
+
/** Tax ID; null for a business with none (under the VAT threshold, say). */
|
|
22
|
+
taxId: z.string().trim().min(1).max(40).nullable(),
|
|
23
|
+
/** ISO 3166-1 alpha-2. */
|
|
24
|
+
country: z.string().regex(/^[A-Z]{2}$/, "country is an ISO 3166-1 alpha-2 code"),
|
|
25
|
+
/** ISO 4217. */
|
|
26
|
+
currency: z.string().regex(/^[A-Z]{3}$/, "currency is an ISO 4217 code"),
|
|
27
|
+
/** IANA time zone. */
|
|
28
|
+
timezone: z.string().refine(isIanaTimeZone, "timezone is not an IANA time zone")
|
|
29
|
+
});
|
|
3
30
|
var CONTROLS = {
|
|
4
31
|
"flag.paymentsSetupStep": { schema: z.boolean(), appWrite: false },
|
|
5
32
|
"plan.locations": {
|
|
@@ -37,6 +64,21 @@ var CONTROLS = {
|
|
|
37
64
|
"plan.accountingEdition": {
|
|
38
65
|
schema: z.enum(["small", "medium"]).nullable(),
|
|
39
66
|
appWrite: false
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* The business this app is for, owned by Stardeck. null means Stardeck has no record yet: an
|
|
70
|
+
* app that already has its own company details sends them up once. `{ ok: false }` means
|
|
71
|
+
* unknown right now: keep the company the app has.
|
|
72
|
+
*/
|
|
73
|
+
"business.profile": { schema: businessProfileSchema.nullable(), appWrite: true },
|
|
74
|
+
/**
|
|
75
|
+
* The org's logo: the icon it set in Stardeck (onboarding asks for one). The app shows it
|
|
76
|
+
* wherever it has no logo of its own, and an owner's own upload always wins. null means the
|
|
77
|
+
* org has no logo. Read-only: the app changes its own logo, never the org's.
|
|
78
|
+
*/
|
|
79
|
+
"business.logo": {
|
|
80
|
+
schema: z.object({ url: z.string().url().startsWith("https://", "the logo is served over https") }).nullable(),
|
|
81
|
+
appWrite: false
|
|
40
82
|
}
|
|
41
83
|
};
|
|
42
84
|
var CONTROL_KEYS = Object.keys(CONTROLS);
|
|
@@ -82,6 +124,7 @@ function unavailableControls(reason) {
|
|
|
82
124
|
export {
|
|
83
125
|
CONTROLS,
|
|
84
126
|
CONTROL_KEYS,
|
|
127
|
+
businessProfileSchema,
|
|
85
128
|
isControlKey,
|
|
86
129
|
isWritableControl,
|
|
87
130
|
parseControls,
|
package/dist/server/index.cjs
CHANGED
|
@@ -32,6 +32,33 @@ module.exports = __toCommonJS(index_exports);
|
|
|
32
32
|
|
|
33
33
|
// src/controls.ts
|
|
34
34
|
var import_zod = require("zod");
|
|
35
|
+
function isIanaTimeZone(value) {
|
|
36
|
+
try {
|
|
37
|
+
new Intl.DateTimeFormat("en", { timeZone: value });
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
var businessProfileSchema = import_zod.z.object({
|
|
44
|
+
/** The kind of business: a business preset id the app defines (`restaurant`, `spa`, `general`…). */
|
|
45
|
+
kind: import_zod.z.string().regex(/^[a-z][a-z0-9-]{0,39}$/, "a business kind is a lowercase preset id"),
|
|
46
|
+
/** The company's name. Thai is null when the business has no Thai name. */
|
|
47
|
+
name: import_zod.z.object({
|
|
48
|
+
en: import_zod.z.string().trim().min(1, "the business needs a name").max(200),
|
|
49
|
+
th: import_zod.z.string().trim().min(1).max(200).nullable()
|
|
50
|
+
}),
|
|
51
|
+
/** Head-office address as one block of text; null until someone enters it. */
|
|
52
|
+
headOfficeAddress: import_zod.z.string().trim().min(1).max(1e3).nullable(),
|
|
53
|
+
/** Tax ID; null for a business with none (under the VAT threshold, say). */
|
|
54
|
+
taxId: import_zod.z.string().trim().min(1).max(40).nullable(),
|
|
55
|
+
/** ISO 3166-1 alpha-2. */
|
|
56
|
+
country: import_zod.z.string().regex(/^[A-Z]{2}$/, "country is an ISO 3166-1 alpha-2 code"),
|
|
57
|
+
/** ISO 4217. */
|
|
58
|
+
currency: import_zod.z.string().regex(/^[A-Z]{3}$/, "currency is an ISO 4217 code"),
|
|
59
|
+
/** IANA time zone. */
|
|
60
|
+
timezone: import_zod.z.string().refine(isIanaTimeZone, "timezone is not an IANA time zone")
|
|
61
|
+
});
|
|
35
62
|
var CONTROLS = {
|
|
36
63
|
"flag.paymentsSetupStep": { schema: import_zod.z.boolean(), appWrite: false },
|
|
37
64
|
"plan.locations": {
|
|
@@ -69,6 +96,21 @@ var CONTROLS = {
|
|
|
69
96
|
"plan.accountingEdition": {
|
|
70
97
|
schema: import_zod.z.enum(["small", "medium"]).nullable(),
|
|
71
98
|
appWrite: false
|
|
99
|
+
},
|
|
100
|
+
/**
|
|
101
|
+
* The business this app is for, owned by Stardeck. null means Stardeck has no record yet: an
|
|
102
|
+
* app that already has its own company details sends them up once. `{ ok: false }` means
|
|
103
|
+
* unknown right now: keep the company the app has.
|
|
104
|
+
*/
|
|
105
|
+
"business.profile": { schema: businessProfileSchema.nullable(), appWrite: true },
|
|
106
|
+
/**
|
|
107
|
+
* The org's logo: the icon it set in Stardeck (onboarding asks for one). The app shows it
|
|
108
|
+
* wherever it has no logo of its own, and an owner's own upload always wins. null means the
|
|
109
|
+
* org has no logo. Read-only: the app changes its own logo, never the org's.
|
|
110
|
+
*/
|
|
111
|
+
"business.logo": {
|
|
112
|
+
schema: import_zod.z.object({ url: import_zod.z.string().url().startsWith("https://", "the logo is served over https") }).nullable(),
|
|
113
|
+
appWrite: false
|
|
72
114
|
}
|
|
73
115
|
};
|
|
74
116
|
var CONTROL_KEYS = Object.keys(CONTROLS);
|
package/dist/server/index.d.cts
CHANGED
|
@@ -66,6 +66,37 @@ declare const CONTROLS: {
|
|
|
66
66
|
}>>;
|
|
67
67
|
readonly appWrite: false;
|
|
68
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* The business this app is for, owned by Stardeck. null means Stardeck has no record yet: an
|
|
71
|
+
* app that already has its own company details sends them up once. `{ ok: false }` means
|
|
72
|
+
* unknown right now: keep the company the app has.
|
|
73
|
+
*/
|
|
74
|
+
readonly "business.profile": {
|
|
75
|
+
readonly schema: z.ZodNullable<z.ZodObject<{
|
|
76
|
+
kind: z.ZodString;
|
|
77
|
+
name: z.ZodObject<{
|
|
78
|
+
en: z.ZodString;
|
|
79
|
+
th: z.ZodNullable<z.ZodString>;
|
|
80
|
+
}, z.core.$strip>;
|
|
81
|
+
headOfficeAddress: z.ZodNullable<z.ZodString>;
|
|
82
|
+
taxId: z.ZodNullable<z.ZodString>;
|
|
83
|
+
country: z.ZodString;
|
|
84
|
+
currency: z.ZodString;
|
|
85
|
+
timezone: z.ZodString;
|
|
86
|
+
}, z.core.$strip>>;
|
|
87
|
+
readonly appWrite: true;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The org's logo: the icon it set in Stardeck (onboarding asks for one). The app shows it
|
|
91
|
+
* wherever it has no logo of its own, and an owner's own upload always wins. null means the
|
|
92
|
+
* org has no logo. Read-only: the app changes its own logo, never the org's.
|
|
93
|
+
*/
|
|
94
|
+
readonly "business.logo": {
|
|
95
|
+
readonly schema: z.ZodNullable<z.ZodObject<{
|
|
96
|
+
url: z.ZodString;
|
|
97
|
+
}, z.core.$strip>>;
|
|
98
|
+
readonly appWrite: false;
|
|
99
|
+
};
|
|
69
100
|
};
|
|
70
101
|
type ControlKey = keyof typeof CONTROLS;
|
|
71
102
|
type ControlValue<K extends ControlKey> = z.infer<(typeof CONTROLS)[K]["schema"]>;
|
package/dist/server/index.d.ts
CHANGED
|
@@ -66,6 +66,37 @@ declare const CONTROLS: {
|
|
|
66
66
|
}>>;
|
|
67
67
|
readonly appWrite: false;
|
|
68
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* The business this app is for, owned by Stardeck. null means Stardeck has no record yet: an
|
|
71
|
+
* app that already has its own company details sends them up once. `{ ok: false }` means
|
|
72
|
+
* unknown right now: keep the company the app has.
|
|
73
|
+
*/
|
|
74
|
+
readonly "business.profile": {
|
|
75
|
+
readonly schema: z.ZodNullable<z.ZodObject<{
|
|
76
|
+
kind: z.ZodString;
|
|
77
|
+
name: z.ZodObject<{
|
|
78
|
+
en: z.ZodString;
|
|
79
|
+
th: z.ZodNullable<z.ZodString>;
|
|
80
|
+
}, z.core.$strip>;
|
|
81
|
+
headOfficeAddress: z.ZodNullable<z.ZodString>;
|
|
82
|
+
taxId: z.ZodNullable<z.ZodString>;
|
|
83
|
+
country: z.ZodString;
|
|
84
|
+
currency: z.ZodString;
|
|
85
|
+
timezone: z.ZodString;
|
|
86
|
+
}, z.core.$strip>>;
|
|
87
|
+
readonly appWrite: true;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The org's logo: the icon it set in Stardeck (onboarding asks for one). The app shows it
|
|
91
|
+
* wherever it has no logo of its own, and an owner's own upload always wins. null means the
|
|
92
|
+
* org has no logo. Read-only: the app changes its own logo, never the org's.
|
|
93
|
+
*/
|
|
94
|
+
readonly "business.logo": {
|
|
95
|
+
readonly schema: z.ZodNullable<z.ZodObject<{
|
|
96
|
+
url: z.ZodString;
|
|
97
|
+
}, z.core.$strip>>;
|
|
98
|
+
readonly appWrite: false;
|
|
99
|
+
};
|
|
69
100
|
};
|
|
70
101
|
type ControlKey = keyof typeof CONTROLS;
|
|
71
102
|
type ControlValue<K extends ControlKey> = z.infer<(typeof CONTROLS)[K]["schema"]>;
|
package/dist/server/index.js
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
// src/controls.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
function isIanaTimeZone(value) {
|
|
4
|
+
try {
|
|
5
|
+
new Intl.DateTimeFormat("en", { timeZone: value });
|
|
6
|
+
return true;
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
var businessProfileSchema = z.object({
|
|
12
|
+
/** The kind of business: a business preset id the app defines (`restaurant`, `spa`, `general`…). */
|
|
13
|
+
kind: z.string().regex(/^[a-z][a-z0-9-]{0,39}$/, "a business kind is a lowercase preset id"),
|
|
14
|
+
/** The company's name. Thai is null when the business has no Thai name. */
|
|
15
|
+
name: z.object({
|
|
16
|
+
en: z.string().trim().min(1, "the business needs a name").max(200),
|
|
17
|
+
th: z.string().trim().min(1).max(200).nullable()
|
|
18
|
+
}),
|
|
19
|
+
/** Head-office address as one block of text; null until someone enters it. */
|
|
20
|
+
headOfficeAddress: z.string().trim().min(1).max(1e3).nullable(),
|
|
21
|
+
/** Tax ID; null for a business with none (under the VAT threshold, say). */
|
|
22
|
+
taxId: z.string().trim().min(1).max(40).nullable(),
|
|
23
|
+
/** ISO 3166-1 alpha-2. */
|
|
24
|
+
country: z.string().regex(/^[A-Z]{2}$/, "country is an ISO 3166-1 alpha-2 code"),
|
|
25
|
+
/** ISO 4217. */
|
|
26
|
+
currency: z.string().regex(/^[A-Z]{3}$/, "currency is an ISO 4217 code"),
|
|
27
|
+
/** IANA time zone. */
|
|
28
|
+
timezone: z.string().refine(isIanaTimeZone, "timezone is not an IANA time zone")
|
|
29
|
+
});
|
|
3
30
|
var CONTROLS = {
|
|
4
31
|
"flag.paymentsSetupStep": { schema: z.boolean(), appWrite: false },
|
|
5
32
|
"plan.locations": {
|
|
@@ -37,6 +64,21 @@ var CONTROLS = {
|
|
|
37
64
|
"plan.accountingEdition": {
|
|
38
65
|
schema: z.enum(["small", "medium"]).nullable(),
|
|
39
66
|
appWrite: false
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* The business this app is for, owned by Stardeck. null means Stardeck has no record yet: an
|
|
70
|
+
* app that already has its own company details sends them up once. `{ ok: false }` means
|
|
71
|
+
* unknown right now: keep the company the app has.
|
|
72
|
+
*/
|
|
73
|
+
"business.profile": { schema: businessProfileSchema.nullable(), appWrite: true },
|
|
74
|
+
/**
|
|
75
|
+
* The org's logo: the icon it set in Stardeck (onboarding asks for one). The app shows it
|
|
76
|
+
* wherever it has no logo of its own, and an owner's own upload always wins. null means the
|
|
77
|
+
* org has no logo. Read-only: the app changes its own logo, never the org's.
|
|
78
|
+
*/
|
|
79
|
+
"business.logo": {
|
|
80
|
+
schema: z.object({ url: z.string().url().startsWith("https://", "the logo is served over https") }).nullable(),
|
|
81
|
+
appWrite: false
|
|
40
82
|
}
|
|
41
83
|
};
|
|
42
84
|
var CONTROL_KEYS = Object.keys(CONTROLS);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/controls",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "Platform-owned app controls (feature flags, plan limits, POS Pro hardware, the Accounting edition, payment provider and methods) for Stardeck customer apps: a typed list plus a cached, signed sync client",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.js",
|