@learncard/types 5.17.6 → 5.18.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learncard/types",
3
- "version": "5.17.6",
3
+ "version": "5.18.0",
4
4
  "description": "Shared types for learn card",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -0,0 +1,216 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * In-App Messages — schema for the LaunchDarkly JSON flag that drives
5
+ * conditional, targeted user prompts across LearnCard apps.
6
+ *
7
+ * The flag value is a versioned list of messages. Each message carries its own
8
+ * targeting predicate tree that is evaluated ON DEVICE, because the client
9
+ * knows things LaunchDarkly's own targeting rules cannot reliably see — the
10
+ * Capacitor platform, the native binary version, and (crucially) the live
11
+ * Capgo/OTA bundle version. Keeping targeting in the JSON also makes the whole
12
+ * system unit-testable without a live LaunchDarkly connection.
13
+ *
14
+ * Consumed by `useInAppMessages()` in `learn-card-base`.
15
+ */
16
+
17
+ /** Runtime platform, as reported by `Capacitor.getPlatform()`. */
18
+ export const inAppMessagePlatformValidator = z.enum(['ios', 'android', 'web']);
19
+ export type InAppMessagePlatform = z.infer<typeof inAppMessagePlatformValidator>;
20
+
21
+ /**
22
+ * Which version number a `version` predicate compares against:
23
+ * - `native` — the installed native binary version (`App.getInfo().version`)
24
+ * - `web` — the web build's package.json version (`__APP_VERSION__`)
25
+ * - `capgo` — the live Capgo/OTA bundle version (`CapacitorUpdater.current()`)
26
+ */
27
+ export const inAppMessageVersionSourceValidator = z.enum(['native', 'web', 'capgo']);
28
+ export type InAppMessageVersionSource = z.infer<typeof inAppMessageVersionSourceValidator>;
29
+
30
+ /** Semantic-version comparison operator (less-than, ..., greater-than). */
31
+ export const inAppMessageVersionOpValidator = z.enum(['lt', 'lte', 'eq', 'gte', 'gt']);
32
+ export type InAppMessageVersionOp = z.infer<typeof inAppMessageVersionOpValidator>;
33
+
34
+ /** Match when the runtime platform is one of the listed platforms. */
35
+ export const platformPredicateValidator = z
36
+ .object({ platform: z.array(inAppMessagePlatformValidator).nonempty() })
37
+ .strict();
38
+
39
+ /** Match when the current user's profile role is one of the listed roles. */
40
+ export const rolePredicateValidator = z.object({ role: z.array(z.string()).nonempty() }).strict();
41
+
42
+ /** Match when a chosen version source satisfies a semver comparison. */
43
+ export const versionPredicateValidator = z
44
+ .object({
45
+ version: z
46
+ .object({
47
+ source: inAppMessageVersionSourceValidator,
48
+ op: inAppMessageVersionOpValidator,
49
+ value: z.string(),
50
+ })
51
+ .strict(),
52
+ })
53
+ .strict();
54
+
55
+ export type InAppMessagePredicate =
56
+ | z.infer<typeof platformPredicateValidator>
57
+ | z.infer<typeof rolePredicateValidator>
58
+ | z.infer<typeof versionPredicateValidator>
59
+ | { all: InAppMessagePredicate[] }
60
+ | { any: InAppMessagePredicate[] }
61
+ | { not: InAppMessagePredicate };
62
+
63
+ export const inAppMessagePredicateValidator: z.ZodType<InAppMessagePredicate> = z.lazy(() =>
64
+ z.union([
65
+ platformPredicateValidator,
66
+ rolePredicateValidator,
67
+ versionPredicateValidator,
68
+ z.object({ all: z.array(inAppMessagePredicateValidator) }).strict(),
69
+ z.object({ any: z.array(inAppMessagePredicateValidator) }).strict(),
70
+ z.object({ not: inAppMessagePredicateValidator }).strict(),
71
+ ])
72
+ );
73
+
74
+ /** Visual weight of an action button — maps to the app's button styles. */
75
+ export const inAppMessageActionStyleValidator = z
76
+ .enum(['primary', 'secondary', 'positive', 'dismiss'])
77
+ .default('secondary');
78
+ export type InAppMessageActionStyle = z.infer<typeof inAppMessageActionStyleValidator>;
79
+
80
+ // Action validators use .passthrough() (old clients tolerate new fields on
81
+ // actions), while predicate validators are .strict() so a typo in targeting
82
+ // fails closed at parse time instead of silently matching everyone.
83
+
84
+ /** Navigate to an in-app route (react-router path). */
85
+ export const internalLinkActionValidator = z
86
+ .object({ type: z.literal('internalLink'), path: z.string() })
87
+ .passthrough();
88
+
89
+ /** Open an external URL (native: Capacitor Browser; web: new tab). */
90
+ export const externalLinkActionValidator = z
91
+ .object({ type: z.literal('externalLink'), url: z.string() })
92
+ .passthrough();
93
+
94
+ /**
95
+ * Open this app's store page, resolved per-platform at runtime:
96
+ * iOS → App Store, Android → Google Play, web → configured fallback URL.
97
+ * Optional overrides win over tenant-config-derived links.
98
+ */
99
+ export const appStoreActionValidator = z
100
+ .object({
101
+ type: z.literal('appStore'),
102
+ iosUrl: z.string().optional(),
103
+ androidUrl: z.string().optional(),
104
+ webUrl: z.string().optional(),
105
+ })
106
+ .passthrough();
107
+
108
+ /** Trigger a Capgo OTA bundle check + download (with progress) + reload. */
109
+ export type AppStoreAction = z.infer<typeof appStoreActionValidator>;
110
+
111
+ export const capgoUpdateActionValidator = z
112
+ .object({ type: z.literal('capgoUpdate') })
113
+ .passthrough();
114
+
115
+ /** Dismiss/close the message without any side effect. */
116
+ export const dismissActionValidator = z.object({ type: z.literal('dismiss') }).passthrough();
117
+
118
+ export const inAppMessageActionTargetValidator = z.union([
119
+ internalLinkActionValidator,
120
+ externalLinkActionValidator,
121
+ appStoreActionValidator,
122
+ capgoUpdateActionValidator,
123
+ dismissActionValidator,
124
+ ]);
125
+ export type InAppMessageActionTarget = z.infer<typeof inAppMessageActionTargetValidator>;
126
+
127
+ export const inAppMessageActionValidator = z
128
+ .object({
129
+ label: z.string(),
130
+ style: inAppMessageActionStyleValidator,
131
+ action: inAppMessageActionTargetValidator,
132
+ /** When true, the message closes after this action runs (default true). */
133
+ closeOnComplete: z.boolean().default(true),
134
+ })
135
+ .passthrough();
136
+ export type InAppMessageAction = z.infer<typeof inAppMessageActionValidator>;
137
+
138
+ export const inAppMessageMediaValidator = z
139
+ .object({
140
+ type: z.enum(['youtube', 'image', 'gif']),
141
+ url: z.string(),
142
+ /** Aspect ratio hint, e.g. "16:9" or "1:1". Defaults to 16:9. */
143
+ aspect: z.string().default('16:9'),
144
+ /** Alt text for images/gifs (accessibility). */
145
+ alt: z.string().optional(),
146
+ })
147
+ .passthrough();
148
+ export type InAppMessageMedia = z.infer<typeof inAppMessageMediaValidator>;
149
+
150
+ /**
151
+ * How often a message may re-appear after being seen/dismissed:
152
+ * - `once` — show a single time, ever (persisted forever)
153
+ * - `session` — at most once per app session (in-memory)
154
+ * - `always` — every eligible render (no suppression)
155
+ * - `{ everyDays: n }` — again only after n days have elapsed
156
+ */
157
+ export const inAppMessageFrequencyValidator = z
158
+ .union([
159
+ z.literal('once'),
160
+ z.literal('session'),
161
+ z.literal('always'),
162
+ z.object({ everyDays: z.number().positive() }).strict(),
163
+ ])
164
+ .default('once');
165
+ export type InAppMessageFrequency = z.infer<typeof inAppMessageFrequencyValidator>;
166
+
167
+ export const inAppMessagePresentationValidator = z
168
+ .enum(['modal', 'banner', 'toast'])
169
+ .default('modal');
170
+ export type InAppMessagePresentation = z.infer<typeof inAppMessagePresentationValidator>;
171
+
172
+ export const inAppMessageValidator = z
173
+ .object({
174
+ /** Stable identifier — used to persist dismissal / frequency state. */
175
+ id: z.string(),
176
+ /** Higher priority wins when multiple messages match. Default 0. */
177
+ priority: z.number().default(0),
178
+ /** When false, the message is required/blocking (no dismiss affordance). */
179
+ dismissible: z.boolean().default(true),
180
+ frequency: inAppMessageFrequencyValidator,
181
+ presentation: inAppMessagePresentationValidator,
182
+ media: inAppMessageMediaValidator.optional(),
183
+ /** Optional emoji hero glyph — rendered only when `media` is not set. */
184
+ emoji: z.string().optional(),
185
+ title: z.string(),
186
+ body: z.string().optional(),
187
+ actions: z.array(inAppMessageActionValidator).default([]),
188
+ /** Targeting predicate tree. Omitted → matches everyone. */
189
+ targeting: inAppMessagePredicateValidator.optional(),
190
+ /** Allow disabling a message without removing it from the flag. */
191
+ enabled: z.boolean().default(true),
192
+ })
193
+ .passthrough();
194
+ export type InAppMessage = z.infer<typeof inAppMessageValidator>;
195
+
196
+ export const inAppMessagesFlagValidator = z
197
+ .object({
198
+ version: z.number().default(1),
199
+ messages: z.array(inAppMessageValidator).default([]),
200
+ })
201
+ .passthrough();
202
+ export type InAppMessagesFlag = z.infer<typeof inAppMessagesFlagValidator>;
203
+
204
+ /** Empty, safe default used when the flag is absent or malformed. */
205
+ export const EMPTY_IN_APP_MESSAGES_FLAG: InAppMessagesFlag = { version: 1, messages: [] };
206
+
207
+ /**
208
+ * Parse an unknown flag value into a validated `InAppMessagesFlag`.
209
+ * Returns the empty flag (never throws) so a malformed flag can never crash
210
+ * the host app.
211
+ */
212
+ export const parseInAppMessagesFlag = (raw: unknown): InAppMessagesFlag => {
213
+ const result = inAppMessagesFlagValidator.safeParse(raw);
214
+
215
+ return result.success ? result.data : EMPTY_IN_APP_MESSAGES_FLAG;
216
+ };
package/src/index.ts CHANGED
@@ -15,3 +15,4 @@ export * from './helpers';
15
15
  export * from './queries';
16
16
  export * from './auth';
17
17
  export * from './bitstring-status-list';
18
+ export * from './inAppMessages';
package/src/lcn.ts CHANGED
@@ -80,6 +80,12 @@ export const LCNProfileValidator = z.object({
80
80
  .optional()
81
81
  .describe('Date of birth of the profile: e.g. "1990-01-01".'),
82
82
  country: z.string().optional().describe('Country for the profile.'),
83
+ locale: z
84
+ .string()
85
+ .optional()
86
+ .describe(
87
+ "BCP-47 language tag (e.g. 'es', 'fr', 'ar') — the user's preferred language for server-sent notifications and emails."
88
+ ),
83
89
  approved: z.boolean().optional().describe('Approval status for the profile.'),
84
90
  });
85
91
  export type LCNProfile = z.infer<typeof LCNProfileValidator>;
@@ -918,6 +924,9 @@ export const LCNNotificationTypeEnumValidator = z.enum([
918
924
  'GUARDIAN_APPROVED',
919
925
  'GUARDIAN_REJECTED',
920
926
  'APP_NOTIFICATION',
927
+ 'CREDENTIAL_REVOKED',
928
+ 'CREDENTIAL_SUSPENDED',
929
+ 'CREDENTIAL_UNSUSPENDED',
921
930
  ]);
922
931
 
923
932
  export type LCNNotificationTypeEnum = z.infer<typeof LCNNotificationTypeEnumValidator>;