@aranova/tracking-next 0.24.0 → 0.25.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/README.md CHANGED
@@ -103,6 +103,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
103
103
  creates React context — the module that calls it is a Client Component. Your root layout
104
104
  stays a Server Component and just renders the client `<TrackingProvider>`.
105
105
 
106
+ `<AdPlatformTracking>` needs no wrapper of your own: the package's React surface is built
107
+ as a `"use client"` entry, so importing it from the package root inside a Server Component
108
+ gives you a real client boundary. (Before 0.25.0 it did not — the directive was stripped at
109
+ bundle time and this example failed with `useEffect is not a function`, which is why some
110
+ sites still carry a hand-rolled client wrapper. Those keep working; they are just no longer
111
+ necessary.)
112
+
106
113
  **No consumer `<Suspense>` boundary is required.** The provider reads `useSearchParams()`
107
114
  only inside its own internal `<Suspense>` boundary, so it never opts the enclosing route out
108
115
  of static prerendering. A fully static page (including one with `export const dynamic = 'error'`)
@@ -172,7 +179,7 @@ export function LeadForm() {
172
179
  }
173
180
  ```
174
181
 
175
- `fields[].value` can be any JSON value: string, number, boolean, null, array, or object and is stored as first-party JSONB. Intentionally submitted lead fields may include raw names, emails, phone numbers, addresses, selections, free-text messages, and submitted file data for first-party analytics and lead operations. Build the array explicitly from the successful submission; the SDK never scrapes arbitrary DOM fields. `File`/`Blob` objects must be converted to a JSON representation, and the complete event metadata must fit the 4 KB limit; upload larger files separately and send their storage reference. Never send passwords, authentication tokens, payment-card/bank credentials, or private keys. Apply the client's privacy notice, consent, retention, and regulated-data requirements. Google offline matching uses normalized, server-side SHA-256-hashed identifiers — not raw free-text/file metadata.
182
+ `fields[].value` can be any JSON value: string, number, boolean, null, array, or object and is stored as first-party JSONB. Intentionally submitted lead fields may include raw names, emails, phone numbers, addresses, selections, free-text messages, and submitted file data for first-party analytics and lead operations. Build the array explicitly from the successful submission; the SDK never scrapes arbitrary DOM fields. `File`/`Blob` objects must be converted to a JSON representation; upload larger files separately and send their storage reference. No byte limit is enforced on event metadata — but a submit fired just before navigation flushes over `keepalive`, which browsers cap near 64 KB and drop silently, so keep inlined payloads small. Never send passwords, authentication tokens, payment-card/bank credentials, or private keys. Apply the client's privacy notice, consent, retention, and regulated-data requirements. Google offline matching uses normalized, server-side SHA-256-hashed identifiers — not raw free-text/file metadata.
176
183
 
177
184
  ### Phone clicks (`tel:` taps) — manual or auto-capture
178
185
 
@@ -597,6 +604,10 @@ Reads are best-effort: network/parse failures degrade to `null`/empty — a blog
597
604
  (`parsePhone`/`toE164`/`formatPhone`/`phoneField`, `usePhoneField`, `PhoneField`); calendar
598
605
  read surface + headless hooks (`CalendarClientProvider`, `useCalendarBusy`, `useSlots`,
599
606
  `useBookingForm`) — booking **writes** are not here, see `/calendar-server`
607
+ - `@aranova/tracking-next/client`: the React surface behind its `"use client"` boundary
608
+ (components, hooks, `createTracking`, `PhoneField`). The root entry re-exports all of it, so
609
+ import from the root as shown above — this subpath exists so the directive survives bundling,
610
+ and is available if you ever want to import the boundary explicitly.
600
611
  - `@aranova/tracking-next/sales`: **React-free** server-safe SDK — `createSalesClient`, money/date helpers, and all sale/customer/config types. Use this in Server Components, route handlers, and Node servers; the root entry re-exports the same symbols for back-compat.
601
612
  - `@aranova/tracking-next/calendar`: **React-free, read-only** — `createCalendarReadClient`, `computeSlots`, timezone helpers, and the booking types. Public key, browser-safe. Deliberately exposes no write verb.
602
613
  - `@aranova/tracking-next/calendar-server`: **`server-only`** — `createCalendarWriteClient` and `createCalendarRoutes()` (drop-in booking route handler). Secret key; importing this from a client component fails the build.
@@ -0,0 +1,350 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, CSSProperties, InputHTMLAttributes, ChangeEvent, FocusEvent } from 'react';
3
+ import { h as ConsentState, f as ConsentChoiceState, g as ConsentSource, T as TrackingParams, G as GtagEnvironmentMap, M as MetaPixelEnvironmentMap, b as TrackingEnvironment, k as PhoneConfig, P as ParsedPhone, l as PhoneDisplayFormat } from './phone-utils-BVzSNBf1.mjs';
4
+ import * as react_jsx_runtime from 'react/jsx-runtime';
5
+ import { T as TrackingConfigReference, C as ConversionConfig } from './tracking-config-runtime-BnUlS_Ae.mjs';
6
+ import { N as TriggerRegistryConfig, Q as TypedTrackingClient } from './ingest-typed-BioxtBJD.mjs';
7
+ import { CountryCode } from 'libphonenumber-js';
8
+ import 'zod';
9
+ import 'src';
10
+
11
+ /**
12
+ * Legacy opt-in-era consent banner.
13
+ *
14
+ * @deprecated PERMANENTLY INERT since consent v2 (opt-out model): it renders
15
+ * only while consent is `pending`, and the effective state is never `pending`
16
+ * anymore, so this component always returns `null`. Tracking is on by default;
17
+ * replace the banner with a footer "cookie preferences" control built on
18
+ * {@link useCookiePreferences} (see the package README). Kept exported so
19
+ * existing integrations keep compiling; scheduled for removal.
20
+ */
21
+ interface ConsentBannerProps {
22
+ /** Body text. Defaults to the standard cookies-for-ad-performance message. */
23
+ message?: ReactNode;
24
+ /** Optional bold title above the body text. */
25
+ title?: ReactNode;
26
+ /** Label for the accept button. Default: `"Accept"`. */
27
+ acceptLabel?: string;
28
+ /** Label for the decline button. Default: `"Decline"`. */
29
+ declineLabel?: string;
30
+ /** Optional link inline with the message (e.g. to a privacy policy). */
31
+ policyHref?: string;
32
+ /** Visible text for {@link policyHref}. Default: `"Learn more"`. */
33
+ policyLabel?: string;
34
+ /**
35
+ * Fires after the consent state is persisted + propagated to gtag. Useful
36
+ * for emitting your own analytics event on the choice.
37
+ */
38
+ onAccept?: () => void;
39
+ onDecline?: () => void;
40
+ /** Where the banner docks. Default: `"bottom"`. */
41
+ position?: "top" | "bottom";
42
+ /**
43
+ * Visual theme. `"auto"` follows `prefers-color-scheme`. Default: `"light"`.
44
+ */
45
+ theme?: "light" | "dark" | "auto";
46
+ /** Class added to the outer wrapper for additional styling hooks. */
47
+ className?: string;
48
+ /** Inline style overrides applied to the outer wrapper after the defaults. */
49
+ style?: CSSProperties;
50
+ }
51
+ /**
52
+ * @deprecated Permanently inert since consent v2 — always renders `null`
53
+ * because the effective consent state is never `pending`. Use a footer
54
+ * control built on {@link useCookiePreferences} instead. See
55
+ * {@link ConsentBannerProps} for details.
56
+ */
57
+ declare function ConsentBanner({ message, title, acceptLabel, declineLabel, policyHref, policyLabel, onAccept, onDecline, position, theme, className, style, }?: ConsentBannerProps): ReactNode;
58
+
59
+ /**
60
+ * Read the captured Google Ads click id from first-party cookies.
61
+ *
62
+ * Returns `null` during SSR and before the client has mounted.
63
+ */
64
+ declare function useGclid(): string | null;
65
+ /**
66
+ * Read all captured attribution parameters from first-party cookies.
67
+ *
68
+ * Values are loaded after mount, so the initial render returns all `null`s.
69
+ */
70
+ declare function useTrackingParams(): TrackingParams;
71
+ /** Options for {@link useCookiePreferences}. */
72
+ interface UseCookiePreferencesOptions {
73
+ /** Days an explicit decline is honored. Defaults to 90. */
74
+ declineTtlDays?: number;
75
+ }
76
+ /**
77
+ * The headless cookie-preferences surface returned by
78
+ * {@link useCookiePreferences}.
79
+ */
80
+ interface UseCookiePreferencesResult {
81
+ /** Effective consent — `granted` unless an unexpired explicit decline exists. */
82
+ state: ConsentChoiceState;
83
+ /** `default` = no valid explicit choice stored; `explicit` = visitor chose. */
84
+ source: ConsentSource;
85
+ /** True when the visitor has made no (valid, unexpired) explicit choice. */
86
+ isDefault: boolean;
87
+ isGranted: boolean;
88
+ isDenied: boolean;
89
+ /** ISO timestamp of the explicit choice; null for the default state. */
90
+ updatedAt: string | null;
91
+ /** ISO expiry of an unexpired decline; null otherwise. */
92
+ expiresAt: string | null;
93
+ /** Explicitly opt out of ad tracking (honored for 90 days by default). */
94
+ optOut: () => void;
95
+ /** Explicitly opt in (never expires). */
96
+ optIn: () => void;
97
+ /** Clear the explicit choice — back to default-granted. */
98
+ reset: () => void;
99
+ }
100
+ /**
101
+ * Headless cookie-preferences hook for the opt-out consent model (consent v2).
102
+ *
103
+ * Tracking is ON by default; this hook is how each client site wires its own
104
+ * footer "Cookie preferences" control (button, dialog, toggle — the packages
105
+ * ship no consent UI). State stays in sync with actions from other components
106
+ * in the same tab (via `onConsentChange`) and other tabs (via `storage`
107
+ * events).
108
+ *
109
+ * ```tsx
110
+ * function CookiePreferences() {
111
+ * const { isDenied, optOut, optIn } = useCookiePreferences();
112
+ * return isDenied ? (
113
+ * <button onClick={optIn}>Enable ad measurement</button>
114
+ * ) : (
115
+ * <button onClick={optOut}>Opt out of ad measurement</button>
116
+ * );
117
+ * }
118
+ * ```
119
+ */
120
+ declare function useCookiePreferences(options?: UseCookiePreferencesOptions): UseCookiePreferencesResult;
121
+ /**
122
+ * Read the current visitor consent state.
123
+ *
124
+ * @deprecated Since consent v2 (opt-out model) the state is never `pending`.
125
+ * Use {@link useCookiePreferences} — it exposes the effective state plus
126
+ * `source` so you can tell a default grant from an explicit one.
127
+ */
128
+ declare function useConsentState(): ConsentState;
129
+ /**
130
+ * Result shape of the deprecated {@link useConsent} hook.
131
+ *
132
+ * @deprecated Use {@link UseCookiePreferencesResult} via
133
+ * {@link useCookiePreferences}. `isPending` is always `false` since consent v2.
134
+ */
135
+ interface UseConsentResult {
136
+ state: ConsentState;
137
+ isPending: boolean;
138
+ isGranted: boolean;
139
+ isDenied: boolean;
140
+ accept: () => void;
141
+ decline: () => void;
142
+ reset: () => void;
143
+ }
144
+ /**
145
+ * Legacy opt-in-era consent hook.
146
+ *
147
+ * @deprecated Since consent v2 tracking defaults ON (opt-out model): the state
148
+ * is never `pending`, so banner UIs gated on `isPending` never render. Use
149
+ * {@link useCookiePreferences} for footer "cookie preferences" controls.
150
+ * `accept` / `decline` still work and map to `optIn` / `optOut`.
151
+ */
152
+ declare function useConsent(): UseConsentResult;
153
+
154
+ /**
155
+ * Props for the combined Next.js ad-platform tag loader.
156
+ *
157
+ * Every field is optional and independent: pass the Google fields, the Meta
158
+ * fields, or both. For each platform a labelled `*Ids` map (ALL loaded) takes
159
+ * precedence over the single `*Id` shortcut.
160
+ */
161
+ interface AdPlatformTrackingProps {
162
+ /** Single Google Ads tag id, e.g. `AW-123456789`. */
163
+ gtagId?: string;
164
+ /** Labelled Google Ads tag map — ALL loaded; wins over `gtagId`. */
165
+ gtagIds?: GtagEnvironmentMap;
166
+ /** R2-authoritative Google tracking config. When set, static gtagId(s) are ignored. */
167
+ trackingConfig?: TrackingConfigReference;
168
+ /** Fire a Google page view from this standalone loader. Leave false when using TrackingProvider. */
169
+ standalonePageView?: boolean;
170
+ /** Single Meta Pixel id, e.g. `123456789012345`. */
171
+ metaPixelId?: string;
172
+ /** Labelled Meta Pixel map — ALL loaded; wins over `metaPixelId`. */
173
+ metaPixelIds?: MetaPixelEnvironmentMap;
174
+ }
175
+ /**
176
+ * Next.js client component that loads the configured ad-platform tags — the
177
+ * Google tag (`gtag`) and/or the Meta Pixel (`fbq`) — with Consent Mode, via
178
+ * `next/script`. Render once in the root layout; omit a platform's props to
179
+ * skip it. Renders no visible UI.
180
+ */
181
+ declare function AdPlatformTracking({ gtagId, gtagIds, trackingConfig, standalonePageView, metaPixelId, metaPixelIds, }: AdPlatformTrackingProps): react_jsx_runtime.JSX.Element;
182
+
183
+ /**
184
+ * Props for the Next.js Google Ads tracking component.
185
+ *
186
+ * Accepts either a single `gtagId` (legacy) or a labelled `gtagIds` map
187
+ * where ALL entries are loaded simultaneously via `gtag('config', ...)`.
188
+ */
189
+ type GoogleAdsTrackingProps = {
190
+ gtagId: string;
191
+ gtagIds?: undefined;
192
+ } | {
193
+ gtagId?: undefined;
194
+ gtagIds: GtagEnvironmentMap;
195
+ };
196
+ /**
197
+ * Next.js client component that loads Google Ads gtag with Consent Mode.
198
+ *
199
+ * Render in the root layout `<head>` when the client site runs paid Google
200
+ * Ads. The component injects Next `<Script>` tags and renders no visible UI.
201
+ */
202
+ declare function GoogleAdsTracking(props: GoogleAdsTrackingProps): react_jsx_runtime.JSX.Element | null;
203
+
204
+ interface CreateTrackingOptions<TRegistry extends TriggerRegistryConfig> {
205
+ /**
206
+ * Public tracking API key issued for this business.
207
+ *
208
+ * This key is safe to expose via `NEXT_PUBLIC_*` env vars.
209
+ */
210
+ apiKey: string;
211
+ /**
212
+ * Tracking endpoint base URL, usually ending in `/tracking`.
213
+ */
214
+ endpoint: string;
215
+ /**
216
+ * Trigger registry that controls automatic events and typed manual events.
217
+ */
218
+ triggers: TRegistry;
219
+ /**
220
+ * Deployment environment label reported in session context.
221
+ */
222
+ environment?: TrackingEnvironment;
223
+ /**
224
+ * Labelled map of Google Ads tag IDs. Included in session context and
225
+ * heartbeat metadata so the dashboard can show what's configured.
226
+ *
227
+ * This does NOT load the gtag scripts — use `<GoogleAdsTracking gtagIds={...} />`
228
+ * in the root layout for that. This option only controls what's reported
229
+ * in the tracking payload context.
230
+ */
231
+ gtagIds?: Record<string, string>;
232
+ /**
233
+ * Validate manual event metadata at runtime before queueing.
234
+ *
235
+ * Enable in development to catch shape bugs. Leave disabled in production so
236
+ * analytics can never throw into the host app.
237
+ */
238
+ debug?: boolean;
239
+ /**
240
+ * Phone-field display + default-country config, read by `usePhoneField` /
241
+ * `<PhoneField>`. Display is customizable; the transmitted value is always E.164.
242
+ */
243
+ phone?: PhoneConfig;
244
+ /**
245
+ * GAP28 / unified-goal: enable real-time on-site conversion firing. When set, the SDK
246
+ * fetches the per-business config from `cdnUrl` (optional offline `baked` fallback) and
247
+ * AUTOMATICALLY fires `gtag('event','conversion')` for any automatic event-goal whose
248
+ * trigger threshold a detector crosses. Omit to keep events analytics-only.
249
+ */
250
+ conversionConfig?: {
251
+ cdnUrl: string;
252
+ baked?: ConversionConfig | null;
253
+ };
254
+ /**
255
+ * R2-authoritative config reference — `ARANOVA_TRACKING_CONFIG` from
256
+ * `tracking-cli gen`. Wins over `conversionConfig`. The object URL is composed
257
+ * from `businessId` + `environment` against the production CDN; spread in a
258
+ * `cdnBaseUrl` to read it from somewhere else, e.g.
259
+ * `{ ...ARANOVA_TRACKING_CONFIG, cdnBaseUrl: process.env.NEXT_PUBLIC_ARANOVA_CDN_BASE_URL }`.
260
+ */
261
+ trackingConfig?: TrackingConfigReference;
262
+ }
263
+ interface TrackingProviderProps {
264
+ /**
265
+ * Application subtree that should have access to the tracking client.
266
+ */
267
+ children: ReactNode;
268
+ }
269
+ interface CreateTrackingResult<TRegistry extends TriggerRegistryConfig> {
270
+ /**
271
+ * Client component provider for the Next.js App Router integration.
272
+ */
273
+ TrackingProvider: (props: TrackingProviderProps) => ReactNode;
274
+ /**
275
+ * Hook that returns the registry-typed tracking client.
276
+ */
277
+ useTracking: () => TypedTrackingClient<TRegistry>;
278
+ }
279
+ /**
280
+ * Next.js App Router version of the `createTracking()` factory. Uses
281
+ * `usePathname` + `useSearchParams` from `next/navigation` for SPA route
282
+ * detection rather than patching `history.pushState`, because Next's
283
+ * router does not always go through the History API for transitions.
284
+ *
285
+ * Usage is identical to the React factory — call this once at app
286
+ * startup and export the returned `TrackingProvider` + `useTracking`
287
+ * from your own module, then import them everywhere else. `GoogleAdsTracking`
288
+ * is still imported separately and placed in the root layout's `<head>`
289
+ * when the client runs paid ads.
290
+ */
291
+ declare function createTracking<TRegistry extends TriggerRegistryConfig>(options: CreateTrackingOptions<TRegistry>): CreateTrackingResult<TRegistry>;
292
+
293
+ /** Resolve the effective phone config (provider value or built-in defaults). */
294
+ declare function usePhoneConfig(): {
295
+ defaultCountry: CountryCode;
296
+ display: PhoneDisplayFormat;
297
+ };
298
+ interface UsePhoneFieldOptions {
299
+ defaultValue?: string;
300
+ /** Overrides the provider's `defaultCountry`. */
301
+ country?: CountryCode;
302
+ /** Overrides the provider's `display` (applied to the settled value on blur). */
303
+ display?: PhoneDisplayFormat;
304
+ /** Notified with the canonical E.164 (or `null`) on every change. */
305
+ onValueChange?: (e164: string | null) => void;
306
+ }
307
+ interface PhoneInputProps {
308
+ value: string;
309
+ onChange: (event: ChangeEvent<HTMLInputElement>) => void;
310
+ onBlur: (event: FocusEvent<HTMLInputElement>) => void;
311
+ type: "tel";
312
+ inputMode: "tel";
313
+ autoComplete: "tel";
314
+ }
315
+ interface PhoneFieldApi {
316
+ /** Display value for the `<input>` (live `AsYouType` while typing). */
317
+ value: string;
318
+ /** Canonical E.164 — what gets transmitted. `null` while invalid/incomplete. */
319
+ e164: string | null;
320
+ isValid: boolean;
321
+ /** Validation message, surfaced only after blur with non-empty invalid input. */
322
+ error: string | null;
323
+ parsed: ParsedPhone;
324
+ /** Spread onto an `<input>`: pre-wires value/onChange/onBlur/type/inputMode/autoComplete. */
325
+ inputProps: PhoneInputProps;
326
+ }
327
+ /** Headless phone field — the client owns the markup. */
328
+ declare function usePhoneField(opts?: UsePhoneFieldOptions): PhoneFieldApi;
329
+ interface PhoneFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> {
330
+ country?: CountryCode;
331
+ /** Controlled display value. */
332
+ value?: string;
333
+ /** Uncontrolled initial value. */
334
+ defaultValue?: string;
335
+ /** Receives the native change event (RHF `register().onChange` or your own); the
336
+ * event's `target.value` is already `AsYouType`-formatted. */
337
+ onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
338
+ /** Receives the canonical E.164 (or `null`) on every change. */
339
+ onE164Change?: (e164: string | null) => void;
340
+ }
341
+ /**
342
+ * Batteries-included phone input. Composes identically with react-hook-form
343
+ * `{...register('phone')}` and with controlled state — the `AsYouType` +
344
+ * mutate-`e.target.value`-before-`onChange` technique lives inside, so RHF and
345
+ * controlled parents both receive the formatted value, and the wire value stays
346
+ * E.164.
347
+ */
348
+ declare const PhoneField: react.ForwardRefExoticComponent<PhoneFieldProps & react.RefAttributes<HTMLInputElement>>;
349
+
350
+ export { AdPlatformTracking, type AdPlatformTrackingProps, ConsentBanner, type ConsentBannerProps, type CreateTrackingOptions, type CreateTrackingResult, GoogleAdsTracking, type GoogleAdsTrackingProps, PhoneField, type PhoneFieldApi, type PhoneFieldProps, type PhoneInputProps, type TrackingProviderProps, type UseConsentResult, type UseCookiePreferencesOptions, type UseCookiePreferencesResult, type UsePhoneFieldOptions, createTracking, useConsent, useConsentState, useCookiePreferences, useGclid, usePhoneConfig, usePhoneField, useTrackingParams };