@leisuresaas/react-native 0.6.2
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/LICENSE +21 -0
- package/README.md +90 -0
- package/package.json +49 -0
- package/src/ads/ad-slot.tsx +131 -0
- package/src/ads/ad.tsx +91 -0
- package/src/ads/bundle-id.ts +9 -0
- package/src/ads/constants.ts +92 -0
- package/src/ads/context.tsx +76 -0
- package/src/ads/creative.tsx +105 -0
- package/src/ads/default-theme.ts +284 -0
- package/src/ads/hooks.ts +156 -0
- package/src/ads/index.ts +32 -0
- package/src/ads/layouts/image.tsx +101 -0
- package/src/ads/layouts/index.tsx +28 -0
- package/src/ads/layouts/shared.tsx +80 -0
- package/src/ads/layouts/text.tsx +77 -0
- package/src/ads/lineup-id.ts +6 -0
- package/src/ads/open-click.ts +61 -0
- package/src/ads/rotation/indicators-context.tsx +31 -0
- package/src/ads/rotation/indicators.tsx +22 -0
- package/src/ads/rotation/shell.tsx +98 -0
- package/src/ads/session-id.ts +18 -0
- package/src/ads/theme.ts +105 -0
- package/src/ads-ui.tsx +40 -0
- package/src/ads.ts +16 -0
- package/src/app-update/context.tsx +380 -0
- package/src/auth-errors.ts +50 -0
- package/src/auth-session.ts +129 -0
- package/src/auth-storage.ts +62 -0
- package/src/auth.tsx +249 -0
- package/src/dev.ts +16 -0
- package/src/drive/types.ts +62 -0
- package/src/drive.ts +355 -0
- package/src/errors.ts +11 -0
- package/src/gateway-url.ts +7 -0
- package/src/hosted-ui-link.ts +265 -0
- package/src/index.ts +161 -0
- package/src/platform-api-base.ts +72 -0
- package/src/platform.ts +16 -0
- package/src/public-ads.ts +105 -0
- package/src/public-app-config.ts +106 -0
- package/src/public-auth.ts +252 -0
- package/src/publishable-key.ts +19 -0
- package/src/push.ts +89 -0
- package/src/types.ts +123 -0
- package/src/userinfo.ts +34 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import * as Linking from "expo-linking";
|
|
2
|
+
import * as WebBrowser from "expo-web-browser";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
/** Default Hosted UI path locale when tag is missing/invalid. */
|
|
6
|
+
export const DEFAULT_HOSTED_UI_LOCALE = "en";
|
|
7
|
+
|
|
8
|
+
const BCP47_RE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
9
|
+
|
|
10
|
+
/** True when a path segment looks like a BCP 47 language tag (no support whitelist). */
|
|
11
|
+
export function isLocalePathSegment(raw?: string | null): boolean {
|
|
12
|
+
const v = (raw ?? "").trim();
|
|
13
|
+
return Boolean(v) && BCP47_RE.test(v) && v.length <= 32;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Canonicalize BCP 47-ish tag (language lower, region upper). */
|
|
17
|
+
function canonicalizeBcp47(raw: string): string {
|
|
18
|
+
const parts = raw.trim().split("-").filter(Boolean);
|
|
19
|
+
if (parts.length === 0) {
|
|
20
|
+
return DEFAULT_HOSTED_UI_LOCALE;
|
|
21
|
+
}
|
|
22
|
+
const out = [parts[0]!.toLowerCase()];
|
|
23
|
+
for (const part of parts.slice(1)) {
|
|
24
|
+
if (part.length === 2 && /^[A-Za-z]{2}$/.test(part)) {
|
|
25
|
+
out.push(part.toUpperCase());
|
|
26
|
+
} else if (part.length === 4 && /^[A-Za-z]{4}$/.test(part)) {
|
|
27
|
+
out.push(part[0]!.toUpperCase() + part.slice(1).toLowerCase());
|
|
28
|
+
} else {
|
|
29
|
+
out.push(part.toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out.join("-");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Map BCP 47-ish tag onto a Hosted UI path locale.
|
|
37
|
+
* No support whitelist: any valid tag is accepted (with common aliases).
|
|
38
|
+
* Missing/invalid tags fall back to {@link DEFAULT_HOSTED_UI_LOCALE} (`en`).
|
|
39
|
+
*/
|
|
40
|
+
export function normalizeHostedUILocale(raw?: string | null): string {
|
|
41
|
+
const v = (raw ?? "").trim().replace(/_/g, "-");
|
|
42
|
+
if (!v || !isLocalePathSegment(v)) {
|
|
43
|
+
return DEFAULT_HOSTED_UI_LOCALE;
|
|
44
|
+
}
|
|
45
|
+
const lower = v.toLowerCase();
|
|
46
|
+
// Convenient aliases for Hosted UI path conventions (not a closed allow-list).
|
|
47
|
+
if (lower === "en" || lower.startsWith("en-")) {
|
|
48
|
+
return "en";
|
|
49
|
+
}
|
|
50
|
+
if (lower === "zh" || lower.startsWith("zh-")) {
|
|
51
|
+
return "zh-CN";
|
|
52
|
+
}
|
|
53
|
+
if (lower === "de" || lower.startsWith("de-")) {
|
|
54
|
+
return "de";
|
|
55
|
+
}
|
|
56
|
+
return canonicalizeBcp47(v);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function pathSegments(url: string): string[] {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = Linking.parse(url);
|
|
62
|
+
return (parsed.path ?? "").replace(/^\//, "").split("/").filter(Boolean);
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** True when URL is a Hosted UI password-reset page (path ends with /reset-password). */
|
|
69
|
+
export function isHostedUIPasswordResetURL(url: string): boolean {
|
|
70
|
+
try {
|
|
71
|
+
const path = (Linking.parse(url).path ?? "").replace(/^\//, "");
|
|
72
|
+
return /(^|\/)reset-password\/?$/.test(path) || /\/reset-password\b/.test(path);
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** True when URL is Hosted UI `/{locale}/open` handoff. */
|
|
79
|
+
export function isHostedUIOpenHandoffURL(url: string): boolean {
|
|
80
|
+
const segs = pathSegments(url);
|
|
81
|
+
if (segs.length >= 2 && isLocalePathSegment(segs[0]) && segs[1] === "open") {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return segs.length === 1 && segs[0] === "open";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Business HTTPS URL from `/{locale}/open?next=…`. */
|
|
88
|
+
export function hostedUIURLFromOpenHandoff(url: string): string | null {
|
|
89
|
+
if (!isHostedUIOpenHandoffURL(url)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const parsed = Linking.parse(url);
|
|
94
|
+
const q = parsed.queryParams?.next;
|
|
95
|
+
const next = typeof q === "string" ? q : Array.isArray(q) ? q[0] : "";
|
|
96
|
+
const trimmed = (next ?? "").trim();
|
|
97
|
+
if (!trimmed) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (/^https:\/\//i.test(trimmed)) {
|
|
101
|
+
return trimmed;
|
|
102
|
+
}
|
|
103
|
+
if (trimmed.startsWith("/")) {
|
|
104
|
+
const base = Linking.parse(url);
|
|
105
|
+
const host = base.hostname;
|
|
106
|
+
if (!host) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const scheme = base.scheme === "http" ? "http" : "https";
|
|
110
|
+
return `${scheme}://${host}${trimmed}`;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** True when deep link path is auth/open (custom scheme from Hosted UI). */
|
|
119
|
+
function isAppOpenPath(url: string): boolean {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = Linking.parse(url);
|
|
122
|
+
const host = (parsed.hostname ?? "").replace(/^\//, "");
|
|
123
|
+
const path = (parsed.path ?? "").replace(/^\//, "");
|
|
124
|
+
if (host === "auth" && (path === "open" || path === "")) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return path === "auth/open" || path.endsWith("/auth/open");
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** HTTPS page URL embedded in `{scheme}://auth/open?url=…`. */
|
|
134
|
+
export function hostedUIURLFromAppOpenLink(url: string): string | null {
|
|
135
|
+
if (!isAppOpenPath(url)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const parsed = Linking.parse(url);
|
|
140
|
+
const q = parsed.queryParams?.url;
|
|
141
|
+
const embedded = typeof q === "string" ? q : Array.isArray(q) ? q[0] : "";
|
|
142
|
+
const trimmed = (embedded ?? "").trim();
|
|
143
|
+
if (!trimmed || !/^https:\/\//i.test(trimmed)) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
return trimmed;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Ensure Hosted UI opens with compact mobile layout when terminal is absent. */
|
|
153
|
+
export function withTerminalMobile(url: string): string {
|
|
154
|
+
const trimmed = url.trim();
|
|
155
|
+
if (!trimmed || !/^https?:\/\//i.test(trimmed)) {
|
|
156
|
+
return trimmed;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const u = new URL(trimmed);
|
|
160
|
+
if (!u.searchParams.get("terminal")) {
|
|
161
|
+
u.searchParams.set("terminal", "mobile");
|
|
162
|
+
}
|
|
163
|
+
return u.toString();
|
|
164
|
+
} catch {
|
|
165
|
+
return trimmed;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Rewrite Hosted UI path locale segment (insert or replace). Invalid locale → `en`. */
|
|
170
|
+
export function withHostedUILocale(url: string, locale?: string | null): string {
|
|
171
|
+
const trimmed = url.trim();
|
|
172
|
+
if (locale == null || !String(locale).trim() || !trimmed || !/^https?:\/\//i.test(trimmed)) {
|
|
173
|
+
return trimmed;
|
|
174
|
+
}
|
|
175
|
+
const normalized = normalizeHostedUILocale(locale);
|
|
176
|
+
try {
|
|
177
|
+
const u = new URL(trimmed);
|
|
178
|
+
const segs = u.pathname.split("/").filter(Boolean);
|
|
179
|
+
if (segs.length > 0 && isLocalePathSegment(segs[0])) {
|
|
180
|
+
segs[0] = normalized;
|
|
181
|
+
} else {
|
|
182
|
+
segs.unshift(normalized);
|
|
183
|
+
}
|
|
184
|
+
u.pathname = "/" + segs.join("/");
|
|
185
|
+
return u.toString();
|
|
186
|
+
} catch {
|
|
187
|
+
return trimmed;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type OpenHostedUIInAppOptions = {
|
|
192
|
+
locale?: string;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Open Hosted UI identity URL inside an in-app browser.
|
|
197
|
+
* Pass the HTTPS Universal Link / App Link URL unchanged — do not strip token.
|
|
198
|
+
* Adds `terminal=mobile` when missing so Hosted UI uses the compact layout.
|
|
199
|
+
* Optionally rewrites path locale to match the app language (unsupported/invalid → `en`).
|
|
200
|
+
* Never opens `/open` itself — resolves `next` to the business page first.
|
|
201
|
+
*/
|
|
202
|
+
export async function openHostedUIInApp(url: string, opts?: OpenHostedUIInAppOptions): Promise<void> {
|
|
203
|
+
let trimmed = url.trim();
|
|
204
|
+
const fromHandoff = hostedUIURLFromOpenHandoff(trimmed);
|
|
205
|
+
if (fromHandoff) {
|
|
206
|
+
trimmed = fromHandoff;
|
|
207
|
+
}
|
|
208
|
+
trimmed = withTerminalMobile(trimmed);
|
|
209
|
+
if (opts?.locale) {
|
|
210
|
+
trimmed = withHostedUILocale(trimmed, opts.locale);
|
|
211
|
+
}
|
|
212
|
+
if (!trimmed) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
await WebBrowser.openBrowserAsync(trimmed, {
|
|
216
|
+
createTask: false,
|
|
217
|
+
showInRecents: true,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Handle an incoming deep link:
|
|
223
|
+
* - HTTPS Hosted UI `/open?next=` → In-App Browser with business URL
|
|
224
|
+
* - HTTPS Hosted UI reset-password → In-App Browser
|
|
225
|
+
* - `{scheme}://auth/open?url=https…` → In-App Browser with embedded URL
|
|
226
|
+
*/
|
|
227
|
+
export async function handleHostedUILink(url: string, opts?: OpenHostedUIInAppOptions): Promise<boolean> {
|
|
228
|
+
const fromOpen = hostedUIURLFromAppOpenLink(url);
|
|
229
|
+
if (fromOpen) {
|
|
230
|
+
await openHostedUIInApp(fromOpen, opts);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (isHostedUIOpenHandoffURL(url) || isHostedUIPasswordResetURL(url)) {
|
|
234
|
+
await openHostedUIInApp(url, opts);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Subscribe to cold-start + foreground links for Hosted UI reset-password / open / auth/open.
|
|
242
|
+
* Call once near the app root (e.g. inside AuthProvider children).
|
|
243
|
+
*/
|
|
244
|
+
export function useHostedUIPasswordResetLink(enabled = true, locale?: string): void {
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
if (!enabled) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const opts = locale ? { locale } : undefined;
|
|
250
|
+
let active = true;
|
|
251
|
+
(async () => {
|
|
252
|
+
const initial = await Linking.getInitialURL();
|
|
253
|
+
if (active && initial) {
|
|
254
|
+
await handleHostedUILink(initial, opts);
|
|
255
|
+
}
|
|
256
|
+
})();
|
|
257
|
+
const sub = Linking.addEventListener("url", ({ url }) => {
|
|
258
|
+
void handleHostedUILink(url, opts);
|
|
259
|
+
});
|
|
260
|
+
return () => {
|
|
261
|
+
active = false;
|
|
262
|
+
sub.remove();
|
|
263
|
+
};
|
|
264
|
+
}, [enabled, locale]);
|
|
265
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
export { buildAdClickUrl } from "./ads";
|
|
2
|
+
export {
|
|
3
|
+
Ad,
|
|
4
|
+
AdBanner,
|
|
5
|
+
AdFooter,
|
|
6
|
+
AdInline,
|
|
7
|
+
AdPreview,
|
|
8
|
+
AdSidebar,
|
|
9
|
+
AdsProvider,
|
|
10
|
+
defaultAdsTheme,
|
|
11
|
+
effectiveRotationMode,
|
|
12
|
+
layoutsForType,
|
|
13
|
+
defaultLayoutForType,
|
|
14
|
+
normalizeAdType,
|
|
15
|
+
normalizeLayout,
|
|
16
|
+
normalizeRotationMode,
|
|
17
|
+
useAdsFeed,
|
|
18
|
+
useAdRotation,
|
|
19
|
+
TEXT_LAYOUTS,
|
|
20
|
+
IMAGE_LAYOUTS,
|
|
21
|
+
AD_TYPES,
|
|
22
|
+
} from "./ads-ui";
|
|
23
|
+
export type {
|
|
24
|
+
AdCreativeStyles,
|
|
25
|
+
AdLayout,
|
|
26
|
+
AdLayoutRenderContext,
|
|
27
|
+
AdLayoutStyles,
|
|
28
|
+
AdPreviewProps,
|
|
29
|
+
AdProps,
|
|
30
|
+
AdRenderContext,
|
|
31
|
+
AdRotationMode,
|
|
32
|
+
AdRotationStyles,
|
|
33
|
+
AdSlotViewProps,
|
|
34
|
+
AdsProviderProps,
|
|
35
|
+
AdsTheme,
|
|
36
|
+
InAppNavigate,
|
|
37
|
+
UseAdsFeedOptions,
|
|
38
|
+
UseAdsFeedResult,
|
|
39
|
+
UseAdRotationResult,
|
|
40
|
+
AdType,
|
|
41
|
+
TextLayout,
|
|
42
|
+
ImageLayout,
|
|
43
|
+
TextLayoutStyles,
|
|
44
|
+
ImageLayoutStyles,
|
|
45
|
+
} from "./ads-ui";
|
|
46
|
+
export { AuthProvider, useAuth, LeisureSaasAuthProvider, useLeisureSaasAuth } from "./auth";
|
|
47
|
+
export type {
|
|
48
|
+
AuthContextValue,
|
|
49
|
+
AuthProviderProps,
|
|
50
|
+
LeisureSaasAuthContextValue,
|
|
51
|
+
LeisureSaasAuthProviderProps,
|
|
52
|
+
} from "./auth";
|
|
53
|
+
export { AuthLoginError } from "./auth-errors";
|
|
54
|
+
export type { AuthLoginErrorCode } from "./auth-errors";
|
|
55
|
+
export { fetchUserInfo } from "./userinfo";
|
|
56
|
+
export type { UserInfo } from "./userinfo";
|
|
57
|
+
export {
|
|
58
|
+
DEFAULT_HOSTED_UI_LOCALE,
|
|
59
|
+
handleHostedUILink,
|
|
60
|
+
hostedUIURLFromAppOpenLink,
|
|
61
|
+
hostedUIURLFromOpenHandoff,
|
|
62
|
+
isHostedUIOpenHandoffURL,
|
|
63
|
+
isHostedUIPasswordResetURL,
|
|
64
|
+
isLocalePathSegment,
|
|
65
|
+
normalizeHostedUILocale,
|
|
66
|
+
openHostedUIInApp,
|
|
67
|
+
useHostedUIPasswordResetLink,
|
|
68
|
+
withHostedUILocale,
|
|
69
|
+
withTerminalMobile,
|
|
70
|
+
} from "./hosted-ui-link";
|
|
71
|
+
export {
|
|
72
|
+
bindPlatformAccount,
|
|
73
|
+
changeProductPassword,
|
|
74
|
+
deleteProductAccount,
|
|
75
|
+
loginProductAccount,
|
|
76
|
+
loginWithPlatformAccount,
|
|
77
|
+
logoutProductAccount,
|
|
78
|
+
refreshProductSession,
|
|
79
|
+
registerProductAccount,
|
|
80
|
+
requestProductEmailVerification,
|
|
81
|
+
requestProductPasswordReset,
|
|
82
|
+
startPlatformAuthorize,
|
|
83
|
+
unbindPlatformAccount,
|
|
84
|
+
} from "./public-auth";
|
|
85
|
+
export type { ProductAuthTokens, PublicAuthContext } from "./public-auth";
|
|
86
|
+
export { getPublicAdsFeed, recordPublicAdEvents } from "./public-ads";
|
|
87
|
+
export type { PublicAdsRequestContext } from "./public-ads";
|
|
88
|
+
export { getPublicAppConfig } from "./public-app-config";
|
|
89
|
+
export type { AppConfigResponse, PublicAppConfigRequestContext } from "./public-app-config";
|
|
90
|
+
export {
|
|
91
|
+
DRIVE_UPLOAD_MAX_AUTO_PUT_BYTES,
|
|
92
|
+
listFsNodes,
|
|
93
|
+
getFsStorageUsage,
|
|
94
|
+
getFsNode,
|
|
95
|
+
createFsFolder,
|
|
96
|
+
renameFsNode,
|
|
97
|
+
moveFsNode,
|
|
98
|
+
deleteFsNode,
|
|
99
|
+
createFsUploadSession,
|
|
100
|
+
finishFsUploadSession,
|
|
101
|
+
createFsContentURL,
|
|
102
|
+
putFsUploadURL,
|
|
103
|
+
uploadFsFile,
|
|
104
|
+
} from "./drive";
|
|
105
|
+
export type {
|
|
106
|
+
DriveRequestContext,
|
|
107
|
+
FsContentURLIntent,
|
|
108
|
+
FsContentURLResult,
|
|
109
|
+
FsNode,
|
|
110
|
+
FsStorageUsage,
|
|
111
|
+
FsUploadSessionResult,
|
|
112
|
+
UploadProgress,
|
|
113
|
+
UploadProgressHandler,
|
|
114
|
+
} from "./drive";
|
|
115
|
+
export { applyPublishableKeyHeaders, resolvePublishableKeyFromEnv } from "./publishable-key";
|
|
116
|
+
export { resolveGatewayUrlFromEnv } from "./gateway-url";
|
|
117
|
+
export { fetchPlatformApiBase, resolvePlatformApiBase } from "./platform-api-base";
|
|
118
|
+
export type { ResolvePlatformApiBaseOptions } from "./platform-api-base";
|
|
119
|
+
export {
|
|
120
|
+
AppUpdateProvider,
|
|
121
|
+
AppVersionSettingsCard,
|
|
122
|
+
useAppUpdate,
|
|
123
|
+
useAppVersionSettings,
|
|
124
|
+
} from "./app-update/context";
|
|
125
|
+
export type {
|
|
126
|
+
AppUpdateInfo,
|
|
127
|
+
AppUpdateLabels,
|
|
128
|
+
AppUpdateProviderProps,
|
|
129
|
+
AppUpdateStatus,
|
|
130
|
+
AppVersionSettingsCardProps,
|
|
131
|
+
AppVersionSettingsState,
|
|
132
|
+
} from "./app-update/context";
|
|
133
|
+
export { lineupIdFromSource } from "./ads/lineup-id";
|
|
134
|
+
export {
|
|
135
|
+
accessNeedsRefresh,
|
|
136
|
+
exchangeAuthorizationCode,
|
|
137
|
+
exchangeMagicToken,
|
|
138
|
+
magicTokenFromURL,
|
|
139
|
+
refreshOAuthTokens,
|
|
140
|
+
} from "./auth-session";
|
|
141
|
+
export type { OAuthTokenResponse } from "./auth-session";
|
|
142
|
+
export { buildEnablePushRegistration, fetchNativeDevicePushToken } from "./push";
|
|
143
|
+
export { devAppleSignedTransaction, devDeviceToken, devGooglePurchaseToken } from "./dev";
|
|
144
|
+
export { LeisureSaasHttpError } from "./errors";
|
|
145
|
+
export { mobilePlatform, adsSurfaceKey, adsSurfaceHeaders } from "./platform";
|
|
146
|
+
export { appBundleId } from "./ads/bundle-id";
|
|
147
|
+
export type {
|
|
148
|
+
AdEventInput,
|
|
149
|
+
AdFeedItem,
|
|
150
|
+
AdFeedRotation,
|
|
151
|
+
AdFeedSource,
|
|
152
|
+
AdsFeedResponse,
|
|
153
|
+
AuthConfig,
|
|
154
|
+
EnablePushOptions,
|
|
155
|
+
Entitlement,
|
|
156
|
+
LeisureSaasAuthConfig,
|
|
157
|
+
MobilePlatform,
|
|
158
|
+
Plan,
|
|
159
|
+
RegisterDeviceTokenInput,
|
|
160
|
+
SubscriptionStatus,
|
|
161
|
+
} from "./types";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { LeisureSaasHttpError } from "./errors";
|
|
2
|
+
import { resolveGatewayUrlFromEnv } from "./gateway-url";
|
|
3
|
+
|
|
4
|
+
const DISCOVERY_PLATFORM_API_KEY = "leisuresaas_platform_api_base";
|
|
5
|
+
|
|
6
|
+
function trimSlash(url: string): string {
|
|
7
|
+
return url.replace(/\/$/, "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fetch LeisureSaas platform API base from OIDC discovery
|
|
12
|
+
* (`leisuresaas_platform_api_base` vendor claim).
|
|
13
|
+
* Prefer {@link resolvePlatformApiBase} so EXPO_PUBLIC_GATEWAY_URL wins.
|
|
14
|
+
*/
|
|
15
|
+
export async function fetchPlatformApiBase(issuer: string): Promise<string> {
|
|
16
|
+
const base = trimSlash(issuer.trim());
|
|
17
|
+
if (!base) {
|
|
18
|
+
throw new Error("issuer is required");
|
|
19
|
+
}
|
|
20
|
+
const res = await fetch(`${base}/.well-known/openid-configuration`, {
|
|
21
|
+
method: "GET",
|
|
22
|
+
headers: { Accept: "application/json" },
|
|
23
|
+
});
|
|
24
|
+
const text = await res.text();
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
throw new LeisureSaasHttpError(res.status, text.trim());
|
|
27
|
+
}
|
|
28
|
+
let doc: Record<string, unknown>;
|
|
29
|
+
try {
|
|
30
|
+
doc = JSON.parse(text) as Record<string, unknown>;
|
|
31
|
+
} catch {
|
|
32
|
+
throw new Error("openid-configuration: invalid JSON");
|
|
33
|
+
}
|
|
34
|
+
const raw = doc[DISCOVERY_PLATFORM_API_KEY];
|
|
35
|
+
const api = typeof raw === "string" ? trimSlash(raw.trim()) : "";
|
|
36
|
+
if (!api) {
|
|
37
|
+
throw new Error(`openid-configuration missing ${DISCOVERY_PLATFORM_API_KEY}`);
|
|
38
|
+
}
|
|
39
|
+
return api;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ResolvePlatformApiBaseOptions = {
|
|
43
|
+
/** Explicit platform API root (wins). */
|
|
44
|
+
gatewayUrl?: string;
|
|
45
|
+
/** OAuth issuer used to fetch discovery when env/prop absent. */
|
|
46
|
+
issuer?: string;
|
|
47
|
+
env?: Record<string, string | undefined>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve platform API base: prop → EXPO_PUBLIC_GATEWAY_URL → discovery via issuer.
|
|
52
|
+
* Never treats OAuth issuer itself as the Public API root.
|
|
53
|
+
*/
|
|
54
|
+
export async function resolvePlatformApiBase(opts: ResolvePlatformApiBaseOptions = {}): Promise<string> {
|
|
55
|
+
const fromProp = opts.gatewayUrl?.trim();
|
|
56
|
+
if (fromProp) {
|
|
57
|
+
return trimSlash(fromProp);
|
|
58
|
+
}
|
|
59
|
+
const fromEnv = resolveGatewayUrlFromEnv(opts.env);
|
|
60
|
+
if (fromEnv) {
|
|
61
|
+
return trimSlash(fromEnv);
|
|
62
|
+
}
|
|
63
|
+
const issuer =
|
|
64
|
+
opts.issuer?.trim() ||
|
|
65
|
+
opts.env?.EXPO_PUBLIC_OAUTH_ISSUER?.trim() ||
|
|
66
|
+
(typeof process !== "undefined" ? process.env.EXPO_PUBLIC_OAUTH_ISSUER?.trim() : "") ||
|
|
67
|
+
"";
|
|
68
|
+
if (!issuer) {
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
return fetchPlatformApiBase(issuer);
|
|
72
|
+
}
|
package/src/platform.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
|
|
3
|
+
import type { MobilePlatform } from "./types";
|
|
4
|
+
|
|
5
|
+
export function mobilePlatform(): MobilePlatform {
|
|
6
|
+
return Platform.OS === "ios" ? "ios" : "android";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Surface key for Public / Integration ads feed (ios | android). */
|
|
10
|
+
export function adsSurfaceKey(): MobilePlatform {
|
|
11
|
+
return mobilePlatform();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function adsSurfaceHeaders(): Record<string, string> {
|
|
15
|
+
return { "X-Ads-Surface-Key": adsSurfaceKey() };
|
|
16
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { LeisureSaasHttpError } from "./errors";
|
|
2
|
+
import { adsSurfaceKey } from "./platform";
|
|
3
|
+
import { applyPublishableKeyHeaders } from "./publishable-key";
|
|
4
|
+
import type { AdEventInput, AdsFeedResponse, MobilePlatform } from "./types";
|
|
5
|
+
|
|
6
|
+
export type PublicAdsRequestContext = {
|
|
7
|
+
gatewayUrl: string;
|
|
8
|
+
publishableKey: string;
|
|
9
|
+
/** Surface key sent as X-Ads-Surface-Key (e.g. ios, android, web). */
|
|
10
|
+
surfaceKey?: MobilePlatform | "web";
|
|
11
|
+
/** @deprecated Use surfaceKey. Gateway requires X-Ads-Surface-Key since v0.5.1. */
|
|
12
|
+
platform?: MobilePlatform | "web";
|
|
13
|
+
bundleId?: string;
|
|
14
|
+
origin?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function trimSlash(url: string): string {
|
|
18
|
+
return url.replace(/\/$/, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveSurfaceKey(ctx: PublicAdsRequestContext): MobilePlatform | "web" {
|
|
22
|
+
return ctx.surfaceKey ?? ctx.platform ?? adsSurfaceKey();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function publicAdsHeaders(ctx: PublicAdsRequestContext, accessToken?: string): Record<string, string> {
|
|
26
|
+
const surfaceKey = resolveSurfaceKey(ctx);
|
|
27
|
+
const headers: Record<string, string> = {
|
|
28
|
+
Accept: "application/json",
|
|
29
|
+
"X-Ads-Surface-Key": surfaceKey,
|
|
30
|
+
};
|
|
31
|
+
applyPublishableKeyHeaders(headers, ctx.publishableKey);
|
|
32
|
+
if (surfaceKey === "web") {
|
|
33
|
+
if (ctx.origin?.trim()) {
|
|
34
|
+
headers.Origin = ctx.origin.trim();
|
|
35
|
+
}
|
|
36
|
+
} else if (ctx.bundleId?.trim()) {
|
|
37
|
+
headers["X-Ads-Bundle-Id"] = ctx.bundleId.trim();
|
|
38
|
+
}
|
|
39
|
+
if (accessToken?.trim()) {
|
|
40
|
+
headers.Authorization = `Bearer ${accessToken.trim()}`;
|
|
41
|
+
}
|
|
42
|
+
return headers;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function publicAdsRequest<T>(
|
|
46
|
+
ctx: PublicAdsRequestContext,
|
|
47
|
+
method: "GET" | "POST",
|
|
48
|
+
path: string,
|
|
49
|
+
body?: unknown,
|
|
50
|
+
accessToken?: string,
|
|
51
|
+
): Promise<T> {
|
|
52
|
+
const base = `${trimSlash(ctx.gatewayUrl)}/v1/public/ads`;
|
|
53
|
+
const url = `${base}${path.startsWith("/") ? path : `/${path}`}`;
|
|
54
|
+
const headers = publicAdsHeaders(ctx, accessToken);
|
|
55
|
+
let payload: string | undefined;
|
|
56
|
+
if (body !== undefined) {
|
|
57
|
+
headers["Content-Type"] = "application/json";
|
|
58
|
+
payload = JSON.stringify(body);
|
|
59
|
+
}
|
|
60
|
+
const res = await fetch(url, { method, headers, body: payload });
|
|
61
|
+
const text = await res.text();
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
throw new LeisureSaasHttpError(res.status, text.trim());
|
|
64
|
+
}
|
|
65
|
+
if (!text) {
|
|
66
|
+
return {} as T;
|
|
67
|
+
}
|
|
68
|
+
return JSON.parse(text) as T;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function getPublicAdsFeed(
|
|
72
|
+
ctx: PublicAdsRequestContext,
|
|
73
|
+
placement = "home_banner",
|
|
74
|
+
): Promise<AdsFeedResponse> {
|
|
75
|
+
const q = encodeURIComponent(placement.trim());
|
|
76
|
+
const resp = await publicAdsRequest<AdsFeedResponse>(ctx, "GET", `/feed?placement=${q}`);
|
|
77
|
+
return { ...resp, ads: resp.ads ?? [] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function recordPublicAdEvents(
|
|
81
|
+
ctx: PublicAdsRequestContext,
|
|
82
|
+
sessionId: string,
|
|
83
|
+
events: AdEventInput[],
|
|
84
|
+
accessToken?: string | null,
|
|
85
|
+
): Promise<number> {
|
|
86
|
+
if (events.length === 0) {
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
const payload = {
|
|
90
|
+
session_id: sessionId.trim(),
|
|
91
|
+
events: events.map((ev) => {
|
|
92
|
+
const lineupId = ev.lineupId?.trim() ?? "";
|
|
93
|
+
return {
|
|
94
|
+
ad_id: ev.adId,
|
|
95
|
+
event_type: ev.eventType,
|
|
96
|
+
...(ev.placementKey ? { placement_key: ev.placementKey } : {}),
|
|
97
|
+
...(lineupId ? { lineup_id: lineupId } : {}),
|
|
98
|
+
};
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
const resp = await publicAdsRequest<{ recorded: number }>(
|
|
102
|
+
ctx, "POST", "/events", payload, accessToken ?? undefined,
|
|
103
|
+
);
|
|
104
|
+
return resp.recorded ?? 0;
|
|
105
|
+
}
|