@netloc8/react 0.1.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 ADDED
@@ -0,0 +1,69 @@
1
+ # @netloc8/react
2
+
3
+ React bindings for the NetLoc8 geolocation SDK. Provides a context Provider,
4
+ `useGeo()` hook, and `<GeoGate>` component for conditional rendering by
5
+ location.
6
+
7
+ > **Tip:** If you're using Next.js, install [`@netloc8/nextjs`](../nextjs/)
8
+ > instead — it re-exports everything from this package and adds server-side
9
+ > proxy and data helpers.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ bun add @netloc8/react
15
+ ```
16
+
17
+ **Peer dependencies:** `react >= 19`
18
+
19
+ ## Usage
20
+
21
+ ### Provider with publishable key (SPA)
22
+
23
+ Wrap your app with `<NetLoc8Provider>` and pass a publishable key. The
24
+ provider will call the NetLoc8 API on mount to fetch geo data, then
25
+ reconcile the browser timezone automatically.
26
+
27
+ ```tsx
28
+ import { NetLoc8Provider, useGeo } from '@netloc8/react';
29
+
30
+ function App() {
31
+ return (
32
+ <NetLoc8Provider publishableKey="pk_...">
33
+ <LocationBanner />
34
+ </NetLoc8Provider>
35
+ );
36
+ }
37
+
38
+ function LocationBanner() {
39
+ const geo = useGeo();
40
+ return <p>Timezone: {geo.timezone}</p>;
41
+ }
42
+ ```
43
+
44
+ ### Conditional rendering with GeoGate
45
+
46
+ ```tsx
47
+ import { GeoGate } from '@netloc8/react';
48
+
49
+ <GeoGate eu={true}>
50
+ <CookieConsentBanner />
51
+ </GeoGate>
52
+
53
+ <GeoGate country={['US', 'CA']} not fallback={<p>Not available in your region</p>}>
54
+ <SpecialOffer />
55
+ </GeoGate>
56
+ ```
57
+
58
+ ## Exports
59
+
60
+ | Export | Description |
61
+ |--------|-------------|
62
+ | `NetLoc8Provider` | Context provider — pass `publishableKey` (SPA) or `initialGeo` (SSR) |
63
+ | `useGeo()` | Hook to read the current `Geo` object |
64
+ | `GeoGate` | Conditionally render children based on location |
65
+ | `GeoContext` | Raw React context (for advanced use) |
66
+
67
+ ## License
68
+
69
+ [Elastic License 2.0 (ELv2)](../../LICENSE)
@@ -0,0 +1,75 @@
1
+
2
+ import { Context, ReactNode } from "react";
3
+ import { Geo } from "@netloc8/netloc8-js";
4
+
5
+ //#region src/provider.d.ts
6
+ interface NetLoc8ProviderProps {
7
+ initialGeo?: Geo;
8
+ publishableKey?: string;
9
+ apiUrl?: string;
10
+ children: ReactNode;
11
+ }
12
+ /**
13
+ * Provider component that makes geolocation data available to all child
14
+ * components via the useGeo() hook.
15
+ *
16
+ * Two usage modes:
17
+ *
18
+ * 1. **Server proxy (Next.js):** Pass `initialGeo` from the server. The
19
+ * provider only reconciles the browser timezone on mount.
20
+ *
21
+ * 2. **Client-side SPA:** Pass `publishableKey` (a `pk_` key). The provider
22
+ * fetches geo data from the API on mount via GET /api/v1/ip/me, then
23
+ * reconciles the browser timezone.
24
+ */
25
+ declare function NetLoc8Provider({
26
+ initialGeo,
27
+ publishableKey,
28
+ apiUrl,
29
+ children
30
+ }: NetLoc8ProviderProps): ReactNode;
31
+ //#endregion
32
+ //#region src/hook.d.ts
33
+ /**
34
+ * Hook to access geolocation data in client components.
35
+ * Must be used inside a <NetLoc8Provider>.
36
+ *
37
+ * @throws {Error} if called outside a NetLoc8Provider.
38
+ */
39
+ declare function useGeo(): Geo;
40
+ //#endregion
41
+ //#region src/gate.d.ts
42
+ interface GeoGateProps {
43
+ country?: string | string[];
44
+ region?: string | string[];
45
+ city?: string | string[];
46
+ eu?: boolean;
47
+ not?: boolean;
48
+ fallback?: ReactNode;
49
+ children: ReactNode;
50
+ }
51
+ /**
52
+ * Conditionally render children based on the user's geolocation.
53
+ *
54
+ * Multiple values can be passed as an array for OR matching within a field.
55
+ * When multiple fields are specified, ALL must match (AND logic).
56
+ */
57
+ declare function GeoGate({
58
+ country,
59
+ region,
60
+ city,
61
+ eu,
62
+ not,
63
+ fallback,
64
+ children
65
+ }: GeoGateProps): ReactNode;
66
+ //#endregion
67
+ //#region src/context.d.ts
68
+ /**
69
+ * Internal sentinel — `null` means no provider is present.
70
+ * The public default is cast to `Geo` so consumers see the correct type.
71
+ */
72
+ declare const GeoContext: Context<Geo>;
73
+ //#endregion
74
+ export { GeoContext, GeoGate, NetLoc8Provider, useGeo };
75
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/provider.tsx","../src/hook.ts","../src/gate.tsx","../src/context.tsx"],"mappings":";;;;;UAWU,oBAAA;EACN,UAAA,GAAa,GAAA;EACb,cAAA;EACA,MAAA;EACA,QAAA,EAAU,SAAA;AAAA;;;;;;;;;AAsCd;;;;;iBAAgB,eAAA,CAAA;EAAkB,UAAA;EAAY,cAAA;EAAgB,MAAA;EAAQ;AAAA,GAAY,oBAAA,GAAuB,SAAA;;;;;;AA9ChF;;;iBCKT,MAAA,CAAA,GAAU,GAAA;;;UCPhB,YAAA;EACN,OAAA;EACA,MAAA;EACA,IAAA;EACA,EAAA;EACA,GAAA;EACA,QAAA,GAAW,SAAA;EACX,QAAA,EAAU,SAAA;AAAA;;;;;;;iBASE,OAAA,CAAA;EACZ,OAAA;EACA,MAAA;EACA,IAAA;EACA,EAAA;EACA,GAAA;EACA,QAAA;EACA;AAAA,GACD,YAAA,GAAe,SAAA;;;;;AFtBO;;cGEZ,UAAA,EAAY,OAAA,CAAQ,GAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,122 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext, useEffect, useState } from "react";
4
+ import { COOKIE_NAME, COOKIE_OPTIONS, fetchMyGeo, normalizeApiResponse, serializeCookie } from "@netloc8/netloc8-js";
5
+ //#region src/context.tsx
6
+ /**
7
+ * Internal sentinel — `null` means no provider is present.
8
+ * The public default is cast to `Geo` so consumers see the correct type.
9
+ */
10
+ const GeoContext = createContext(null);
11
+ //#endregion
12
+ //#region src/provider.tsx
13
+ /**
14
+ * Write the geo cookie using the shared COOKIE_OPTIONS constants.
15
+ */
16
+ function writeGeoCookie(geo) {
17
+ const parts = [`${COOKIE_NAME}=${serializeCookie(geo)}`, `path=${COOKIE_OPTIONS.path}`];
18
+ if (COOKIE_OPTIONS.secure && globalThis.location?.protocol === "https:") parts.push("secure");
19
+ if (COOKIE_OPTIONS.sameSite) parts.push(`samesite=${COOKIE_OPTIONS.sameSite}`);
20
+ if (COOKIE_OPTIONS.maxAge !== void 0) parts.push(`max-age=${COOKIE_OPTIONS.maxAge}`);
21
+ document.cookie = parts.join("; ");
22
+ }
23
+ /**
24
+ * Provider component that makes geolocation data available to all child
25
+ * components via the useGeo() hook.
26
+ *
27
+ * Two usage modes:
28
+ *
29
+ * 1. **Server proxy (Next.js):** Pass `initialGeo` from the server. The
30
+ * provider only reconciles the browser timezone on mount.
31
+ *
32
+ * 2. **Client-side SPA:** Pass `publishableKey` (a `pk_` key). The provider
33
+ * fetches geo data from the API on mount via GET /api/v1/ip/me, then
34
+ * reconciles the browser timezone.
35
+ */
36
+ function NetLoc8Provider({ initialGeo, publishableKey, apiUrl, children }) {
37
+ const [geo, setGeo] = useState(initialGeo ?? {});
38
+ useEffect(() => {
39
+ let cancelled = false;
40
+ async function init() {
41
+ let currentGeo = initialGeo ?? {};
42
+ if (publishableKey && !initialGeo) {
43
+ const raw = await fetchMyGeo({
44
+ apiKey: publishableKey,
45
+ apiUrl,
46
+ clientId: `@netloc8/react/0.1.0`
47
+ });
48
+ if (cancelled) return;
49
+ if (raw) {
50
+ currentGeo = normalizeApiResponse(raw);
51
+ setGeo(currentGeo);
52
+ writeGeoCookie(currentGeo);
53
+ }
54
+ }
55
+ const browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
56
+ if (browserTz !== currentGeo.timezone || currentGeo.timezoneFromClient !== true) {
57
+ if (!cancelled) setGeo((prevGeo) => {
58
+ const updatedGeo = {
59
+ ...prevGeo,
60
+ timezone: browserTz,
61
+ timezoneFromClient: true
62
+ };
63
+ writeGeoCookie(updatedGeo);
64
+ return updatedGeo;
65
+ });
66
+ }
67
+ }
68
+ init();
69
+ return () => {
70
+ cancelled = true;
71
+ };
72
+ }, []);
73
+ return /* @__PURE__ */ jsx(GeoContext.Provider, {
74
+ value: geo,
75
+ children
76
+ });
77
+ }
78
+ //#endregion
79
+ //#region src/hook.ts
80
+ /**
81
+ * Hook to access geolocation data in client components.
82
+ * Must be used inside a <NetLoc8Provider>.
83
+ *
84
+ * @throws {Error} if called outside a NetLoc8Provider.
85
+ */
86
+ function useGeo() {
87
+ const geo = useContext(GeoContext);
88
+ if (geo === null) throw new Error("useGeo() must be used inside a <NetLoc8Provider>.");
89
+ return geo;
90
+ }
91
+ //#endregion
92
+ //#region src/gate.tsx
93
+ /**
94
+ * Conditionally render children based on the user's geolocation.
95
+ *
96
+ * Multiple values can be passed as an array for OR matching within a field.
97
+ * When multiple fields are specified, ALL must match (AND logic).
98
+ */
99
+ function GeoGate({ country, region, city, eu, not = false, fallback = null, children }) {
100
+ const geo = useGeo();
101
+ const checks = [];
102
+ if (eu !== void 0) checks.push(geo.isEU === eu);
103
+ if (country !== void 0) {
104
+ const countries = Array.isArray(country) ? country : [country];
105
+ checks.push(geo.country !== void 0 && countries.includes(geo.country));
106
+ }
107
+ if (region !== void 0) {
108
+ const regions = Array.isArray(region) ? region : [region];
109
+ checks.push(geo.region !== void 0 && regions.includes(geo.region));
110
+ }
111
+ if (city !== void 0) {
112
+ const cities = Array.isArray(city) ? city : [city];
113
+ checks.push(geo.city !== void 0 && cities.includes(geo.city));
114
+ }
115
+ let matches = checks.length === 0 || checks.every(Boolean);
116
+ if (not) matches = !matches;
117
+ return matches ? children : fallback;
118
+ }
119
+ //#endregion
120
+ export { GeoContext, GeoGate, NetLoc8Provider, useGeo };
121
+
122
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/context.tsx","../src/provider.tsx","../src/hook.ts","../src/gate.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, type Context } from 'react';\nimport type { Geo } from '@netloc8/netloc8-js';\n\n/**\n * Internal sentinel — `null` means no provider is present.\n * The public default is cast to `Geo` so consumers see the correct type.\n */\nexport const GeoContext: Context<Geo> = createContext<Geo>(null as unknown as Geo);\n","declare const __PKG_NAME__: string;\ndeclare const __PKG_VERSION__: string;\n\n'use client';\n\nimport { useState, useEffect } from 'react';\nimport type { ReactNode } from 'react';\nimport type { Geo } from '@netloc8/netloc8-js';\nimport { COOKIE_NAME, COOKIE_OPTIONS, serializeCookie, fetchMyGeo, normalizeApiResponse } from '@netloc8/netloc8-js';\nimport { GeoContext } from './context';\n\ninterface NetLoc8ProviderProps {\n initialGeo?: Geo;\n publishableKey?: string;\n apiUrl?: string;\n children: ReactNode;\n}\n\n/**\n * Write the geo cookie using the shared COOKIE_OPTIONS constants.\n */\nfunction writeGeoCookie(geo: Geo): void {\n const value = serializeCookie(geo);\n const parts = [`${COOKIE_NAME}=${value}`, `path=${COOKIE_OPTIONS.path}`];\n\n if (COOKIE_OPTIONS.secure && globalThis.location?.protocol === 'https:') {\n parts.push('secure');\n }\n\n if (COOKIE_OPTIONS.sameSite) {\n parts.push(`samesite=${COOKIE_OPTIONS.sameSite}`);\n }\n\n if (COOKIE_OPTIONS.maxAge !== undefined) {\n parts.push(`max-age=${COOKIE_OPTIONS.maxAge}`);\n }\n\n document.cookie = parts.join('; ');\n}\n\n/**\n * Provider component that makes geolocation data available to all child\n * components via the useGeo() hook.\n *\n * Two usage modes:\n *\n * 1. **Server proxy (Next.js):** Pass `initialGeo` from the server. The\n * provider only reconciles the browser timezone on mount.\n *\n * 2. **Client-side SPA:** Pass `publishableKey` (a `pk_` key). The provider\n * fetches geo data from the API on mount via GET /api/v1/ip/me, then\n * reconciles the browser timezone.\n */\nexport function NetLoc8Provider({ initialGeo, publishableKey, apiUrl, children }: NetLoc8ProviderProps): ReactNode {\n const [geo, setGeo] = useState<Geo>(initialGeo ?? {});\n\n useEffect(() => {\n let cancelled = false;\n\n async function init() {\n let currentGeo: Geo = initialGeo ?? {};\n\n // Client-side fetch when publishableKey is provided and no server data\n if (publishableKey && !initialGeo) {\n const raw = await fetchMyGeo({ apiKey: publishableKey, apiUrl, clientId: typeof __PKG_NAME__ !== 'undefined' ? `${__PKG_NAME__}/${__PKG_VERSION__}` : undefined });\n\n if (cancelled) {\n return;\n }\n\n if (raw) {\n currentGeo = normalizeApiResponse(raw);\n setGeo(currentGeo);\n writeGeoCookie(currentGeo);\n }\n }\n\n // Timezone reconciliation — runs in both modes\n const browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone;\n\n if (browserTz !== currentGeo.timezone || currentGeo.timezoneFromClient !== true) {\n if (!cancelled) {\n setGeo((prevGeo: Geo) => {\n const updatedGeo: Geo = {\n ...prevGeo,\n timezone: browserTz,\n timezoneFromClient: true,\n };\n\n writeGeoCookie(updatedGeo);\n return updatedGeo;\n });\n }\n }\n }\n\n init();\n\n return () => {\n cancelled = true;\n };\n }, []); // Only run once on mount\n\n return (\n <GeoContext.Provider value={geo}>\n {children}\n </GeoContext.Provider>\n );\n}\n","'use client';\n\nimport { useContext } from 'react';\nimport { GeoContext } from './context';\nimport type { Geo } from '@netloc8/netloc8-js';\n\n/**\n * Hook to access geolocation data in client components.\n * Must be used inside a <NetLoc8Provider>.\n *\n * @throws {Error} if called outside a NetLoc8Provider.\n */\nexport function useGeo(): Geo {\n const geo = useContext(GeoContext);\n\n if (geo === null) {\n throw new Error('useGeo() must be used inside a <NetLoc8Provider>.');\n }\n\n return geo;\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { useGeo } from './hook';\n\ninterface GeoGateProps {\n country?: string | string[];\n region?: string | string[];\n city?: string | string[];\n eu?: boolean;\n not?: boolean;\n fallback?: ReactNode;\n children: ReactNode;\n}\n\n/**\n * Conditionally render children based on the user's geolocation.\n *\n * Multiple values can be passed as an array for OR matching within a field.\n * When multiple fields are specified, ALL must match (AND logic).\n */\nexport function GeoGate({\n country,\n region,\n city,\n eu,\n not = false,\n fallback = null,\n children,\n}: GeoGateProps): ReactNode {\n const geo = useGeo();\n\n const checks: boolean[] = [];\n\n if (eu !== undefined) {\n checks.push(geo.isEU === eu);\n }\n\n if (country !== undefined) {\n const countries = Array.isArray(country) ? country : [country];\n checks.push(geo.country !== undefined && countries.includes(geo.country));\n }\n\n if (region !== undefined) {\n const regions = Array.isArray(region) ? region : [region];\n checks.push(geo.region !== undefined && regions.includes(geo.region));\n }\n\n if (city !== undefined) {\n const cities = Array.isArray(city) ? city : [city];\n checks.push(geo.city !== undefined && cities.includes(geo.city));\n }\n\n // If no props specified, matches everything\n let matches = checks.length === 0 || checks.every(Boolean);\n\n if (not) {\n matches = !matches;\n }\n\n return matches ? children : fallback;\n}\n"],"mappings":";;;;;;;;;AASA,MAAa,aAA2B,cAAmB,KAAuB;;;;;;ACYlF,SAAS,eAAe,KAAgB;CAEpC,MAAM,QAAQ,CAAC,GAAG,YAAY,GADhB,gBAAgB,IAAI,IACQ,QAAQ,eAAe,OAAO;AAExE,KAAI,eAAe,UAAU,WAAW,UAAU,aAAa,SAC3D,OAAM,KAAK,SAAS;AAGxB,KAAI,eAAe,SACf,OAAM,KAAK,YAAY,eAAe,WAAW;AAGrD,KAAI,eAAe,WAAW,KAAA,EAC1B,OAAM,KAAK,WAAW,eAAe,SAAS;AAGlD,UAAS,SAAS,MAAM,KAAK,KAAK;;;;;;;;;;;;;;;AAgBtC,SAAgB,gBAAgB,EAAE,YAAY,gBAAgB,QAAQ,YAA6C;CAC/G,MAAM,CAAC,KAAK,UAAU,SAAc,cAAc,EAAE,CAAC;AAErD,iBAAgB;EACZ,IAAI,YAAY;EAEhB,eAAe,OAAO;GAClB,IAAI,aAAkB,cAAc,EAAE;AAGtC,OAAI,kBAAkB,CAAC,YAAY;IAC/B,MAAM,MAAM,MAAM,WAAW;KAAE,QAAQ;KAAgB;KAAQ,UAAgD;KAAkD,CAAC;AAElK,QAAI,UACA;AAGJ,QAAI,KAAK;AACL,kBAAa,qBAAqB,IAAI;AACtC,YAAO,WAAW;AAClB,oBAAe,WAAW;;;GAKlC,MAAM,YAAY,KAAK,gBAAgB,CAAC,iBAAiB,CAAC;AAE1D,OAAI,cAAc,WAAW,YAAY,WAAW,uBAAuB;QACnE,CAAC,UACD,SAAQ,YAAiB;KACrB,MAAM,aAAkB;MACpB,GAAG;MACH,UAAU;MACV,oBAAoB;MACvB;AAED,oBAAe,WAAW;AAC1B,YAAO;MACT;;;AAKd,QAAM;AAEN,eAAa;AACT,eAAY;;IAEjB,EAAE,CAAC;AAEN,QACI,oBAAC,WAAW,UAAZ;EAAqB,OAAO;EACvB;EACiB,CAAA;;;;;;;;;;AC9F9B,SAAgB,SAAc;CAC1B,MAAM,MAAM,WAAW,WAAW;AAElC,KAAI,QAAQ,KACR,OAAM,IAAI,MAAM,oDAAoD;AAGxE,QAAO;;;;;;;;;;ACEX,SAAgB,QAAQ,EACpB,SACA,QACA,MACA,IACA,MAAM,OACN,WAAW,MACX,YACwB;CACxB,MAAM,MAAM,QAAQ;CAEpB,MAAM,SAAoB,EAAE;AAE5B,KAAI,OAAO,KAAA,EACP,QAAO,KAAK,IAAI,SAAS,GAAG;AAGhC,KAAI,YAAY,KAAA,GAAW;EACvB,MAAM,YAAY,MAAM,QAAQ,QAAQ,GAAG,UAAU,CAAC,QAAQ;AAC9D,SAAO,KAAK,IAAI,YAAY,KAAA,KAAa,UAAU,SAAS,IAAI,QAAQ,CAAC;;AAG7E,KAAI,WAAW,KAAA,GAAW;EACtB,MAAM,UAAU,MAAM,QAAQ,OAAO,GAAG,SAAS,CAAC,OAAO;AACzD,SAAO,KAAK,IAAI,WAAW,KAAA,KAAa,QAAQ,SAAS,IAAI,OAAO,CAAC;;AAGzE,KAAI,SAAS,KAAA,GAAW;EACpB,MAAM,SAAS,MAAM,QAAQ,KAAK,GAAG,OAAO,CAAC,KAAK;AAClD,SAAO,KAAK,IAAI,SAAS,KAAA,KAAa,OAAO,SAAS,IAAI,KAAK,CAAC;;CAIpE,IAAI,UAAU,OAAO,WAAW,KAAK,OAAO,MAAM,QAAQ;AAE1D,KAAI,IACA,WAAU,CAAC;AAGf,QAAO,UAAU,WAAW"}
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@netloc8/react",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "Elastic-2.0",
6
+ "exports": {
7
+ ".": {
8
+ "bun": "./src/index.ts",
9
+ "import": "./dist/index.mjs",
10
+ "types": "./dist/index.d.mts"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist/"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsdown",
18
+ "clean": "rm -rf dist",
19
+ "test": "bun test src/"
20
+ },
21
+ "peerDependencies": {
22
+ "react": ">=19.0.0"
23
+ },
24
+ "dependencies": {
25
+ "@netloc8/netloc8-js": "0.1.0"
26
+ }
27
+ }