@sanvika/geolocation 0.5.0 → 0.6.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": "@sanvika/geolocation",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Sanvika ecosystem geolocation SDK — fully centralized via geolocation.sanvikaproduction.com. IP lookup, reverse/forward geocoding, Places autocomplete + details, Maps JS loader. Zero Google API keys in consumer projects.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -8,14 +8,30 @@
8
8
  "exports": {
9
9
  ".": "./src/index.js",
10
10
  "./server": "./src/server/index.js",
11
- "./client": "./src/client/index.js"
11
+ "./client": "./src/client/index.js",
12
+ "./react-native": "./src/react-native/index.js"
13
+ },
14
+ "peerDependencies": {
15
+ "react-native": "*",
16
+ "@react-native-async-storage/async-storage": "*",
17
+ "@react-native-community/geolocation": "*",
18
+ "react-native-permissions": "*"
19
+ },
20
+ "peerDependenciesMeta": {
21
+ "react-native": { "optional": true },
22
+ "@react-native-async-storage/async-storage": { "optional": true },
23
+ "@react-native-community/geolocation": { "optional": true },
24
+ "react-native-permissions": { "optional": true }
12
25
  },
13
26
  "files": [
14
27
  "src",
15
28
  "README.md"
16
29
  ],
17
30
  "dependencies": {
18
- "@sanvika/logger": ">=0.2"
31
+ "@sanvika/geolocation": "0.5.0",
32
+ "@sanvika/lang": "0.6.0",
33
+ "@sanvika/logger": ">=0.2",
34
+ "@sanvika/realtime": "0.7.0"
19
35
  },
20
36
  "keywords": [
21
37
  "sanvika",
@@ -33,4 +49,4 @@
33
49
  "publishConfig": {
34
50
  "access": "public"
35
51
  }
36
- }
52
+ }
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // Env vars (read from process.env in Next.js / similar):
6
6
  // NEXT_PUBLIC_GEOLOCATION_URL=https://geolocation.sanvikaproduction.com
7
- // NEXT_PUBLIC_PROJECT_NAME=<slug> (universal, Phase 2 — ecosystem rule §21)
7
+ // NEXT_PUBLIC_CLIENT_ID=<slug> (universal)
8
8
  // (fallback) NEXT_PUBLIC_GEOLOCATION_CLIENT_ID
9
9
  //
10
10
  // Consumer projects NO LONGER need proxy routes for geolocation.
@@ -23,7 +23,7 @@ function _geoUrl() {
23
23
  function _clientId() {
24
24
  // Phase 2 universal first, then legacy fallback (Next.js requires static access)
25
25
  if (typeof process === "undefined" || !process.env) return "";
26
- return process.env.NEXT_PUBLIC_PROJECT_NAME
26
+ return process.env.NEXT_PUBLIC_CLIENT_ID
27
27
  || process.env.NEXT_PUBLIC_GEOLOCATION_CLIENT_ID
28
28
  || "";
29
29
  }
@@ -50,10 +50,17 @@ export async function loadGoogleMapsScript({
50
50
  if (window.google?.maps?.Map) return window.google;
51
51
  if (inflight) return inflight;
52
52
 
53
- const apiBase = (apiBaseInput || readEnv("NEXT_PUBLIC_GEOLOCATION_URL") || "").replace(/\/$/, "");
54
- const clientId = clientIdInput || readEnv("NEXT_PUBLIC_GEOLOCATION_CLIENT_ID");
55
- if (!apiBase || !clientId) {
56
- throw new Error("loadGoogleMapsScript: NEXT_PUBLIC_GEOLOCATION_URL and NEXT_PUBLIC_GEOLOCATION_CLIENT_ID are required");
53
+ const apiBase = (
54
+ apiBaseInput ||
55
+ readEnv("NEXT_PUBLIC_GEOLOCATION_URL") ||
56
+ "https://geolocation.sanvikaproduction.com"
57
+ ).replace(/\/$/, "");
58
+ const clientId =
59
+ clientIdInput ||
60
+ readEnv("NEXT_PUBLIC_CLIENT_ID") ||
61
+ readEnv("NEXT_PUBLIC_GEOLOCATION_CLIENT_ID");
62
+ if (!clientId) {
63
+ throw new Error("loadGoogleMapsScript: NEXT_PUBLIC_CLIENT_ID (or NEXT_PUBLIC_GEOLOCATION_CLIENT_ID) is required");
57
64
  }
58
65
 
59
66
  inflight = (async () => {
@@ -0,0 +1,33 @@
1
+ // react-native/asyncStorageTTL.js — AsyncStorage-backed TTL cache (mirrors localStorageTTL.js)
2
+ import AsyncStorage from "@react-native-async-storage/async-storage";
3
+
4
+ export const TTL = {
5
+ ONE_HOUR: 60 * 60 * 1000,
6
+ SIX_HOURS: 6 * 60 * 60 * 1000,
7
+ ONE_DAY: 24 * 60 * 60 * 1000,
8
+ };
9
+
10
+ export async function setWithTTL(key, value, ttlMs) {
11
+ try {
12
+ await AsyncStorage.setItem(key, JSON.stringify({ value, expiresAt: Date.now() + ttlMs }));
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ export async function getWithTTL(key) {
20
+ try {
21
+ const raw = await AsyncStorage.getItem(key);
22
+ if (!raw) { return null; }
23
+ const item = JSON.parse(raw);
24
+ if (Date.now() > item.expiresAt) {
25
+ await AsyncStorage.removeItem(key);
26
+ return null;
27
+ }
28
+ return item.value;
29
+ } catch {
30
+ try { await AsyncStorage.removeItem(key); } catch { /* ignore */ }
31
+ return null;
32
+ }
33
+ }
@@ -0,0 +1,21 @@
1
+ // @sanvika/geolocation/react-native — RN subpath entry
2
+ // Universal server-side helpers (no browser deps)
3
+ export {
4
+ INDIAN_STATES,
5
+ INDIAN_UNION_TERRITORIES,
6
+ ALL_INDIAN_REGIONS,
7
+ STATE_NAME_ALIASES,
8
+ normalizeStateName,
9
+ isValidIndianState,
10
+ isPrivateIp,
11
+ clearLocationCache,
12
+ clearStateCache,
13
+ getStateCacheStats,
14
+ } from "../server/index.js";
15
+
16
+ // AsyncStorage-backed TTL cache (mirrors ./client localStorageTTL)
17
+ export { setWithTTL, getWithTTL, TTL } from "./asyncStorageTTL.js";
18
+
19
+ // RN-native resolveLocation (replaces local mobile utils/location/resolveLocation.js)
20
+ export { resolveLocation } from "./resolveLocation.js";
21
+ export { resolveLocation as default } from "./resolveLocation.js";
@@ -0,0 +1,129 @@
1
+ // react-native/resolveLocation.js
2
+ // RN equivalent of mobile's local utils/location/resolveLocation.js.
3
+ // Uses @react-native-community/geolocation (already installed in consumer).
4
+ import Geolocation from "@react-native-community/geolocation";
5
+ import { Platform } from "react-native";
6
+ import { request, PERMISSIONS, RESULTS } from "react-native-permissions";
7
+ import { createLogger } from "@sanvika/logger";
8
+
9
+ const FAPK_API = "https://freeadpostkaro.com/api";
10
+
11
+ const PRIORITY = { USER_DATABASE: 1, DEVICE: 2, IP: 3 };
12
+
13
+ const ensureLog = (log) => log || createLogger({ namespace: "resolveLocation" });
14
+
15
+ const isValidCoord = (v) => typeof v === "number" && Number.isFinite(v);
16
+
17
+ function normalizeCoords(lat, lng) {
18
+ const latitude = typeof lat === "string" ? parseFloat(lat) : lat;
19
+ const longitude = typeof lng === "string" ? parseFloat(lng) : lng;
20
+ return isValidCoord(latitude) && isValidCoord(longitude) ? { latitude, longitude } : null;
21
+ }
22
+
23
+ async function fetchUserDatabaseLocation(uid, apiBaseUrl, log) {
24
+ try {
25
+ const url = `${apiBaseUrl}/location/getUserLocation`;
26
+ const res = await fetch(`${url}?uid=${encodeURIComponent(uid)}`, {
27
+ headers: { uid, Accept: "application/json" },
28
+ });
29
+ if (!res.ok) { return null; }
30
+ const data = await res.json();
31
+ if (!data?.success || !data?.data) { return null; }
32
+ const coords = normalizeCoords(data.data.latitude, data.data.longitude);
33
+ if (!coords) { return null; }
34
+ return { ...data.data, ...coords, source: "user_database", priority: PRIORITY.USER_DATABASE };
35
+ } catch (err) {
36
+ log.error("User DB location fetch failed", { uid, message: err?.message });
37
+ return null;
38
+ }
39
+ }
40
+
41
+ async function fetchDeviceLocation(log) {
42
+ try {
43
+ const permType =
44
+ Platform.OS === "ios"
45
+ ? PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
46
+ : PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION;
47
+
48
+ const result = await request(permType);
49
+ if (result !== RESULTS.GRANTED) {
50
+ log.warn("Device location permission denied", { status: result });
51
+ return null;
52
+ }
53
+
54
+ const position = await new Promise((resolve, reject) => {
55
+ Geolocation.getCurrentPosition(resolve, reject, {
56
+ enableHighAccuracy: true,
57
+ timeout: 10000,
58
+ maximumAge: 1000,
59
+ });
60
+ });
61
+
62
+ const coords = normalizeCoords(position?.coords?.latitude, position?.coords?.longitude);
63
+ if (!coords) { return null; }
64
+
65
+ return {
66
+ ...coords,
67
+ accuracy: position?.coords?.accuracy,
68
+ timestamp: position?.timestamp || Date.now(),
69
+ source: "device_geolocation",
70
+ priority: PRIORITY.DEVICE,
71
+ };
72
+ } catch (err) {
73
+ log.warn("Device geolocation failed", { message: err?.message });
74
+ return null;
75
+ }
76
+ }
77
+
78
+ async function fetchIpLocation(apiBaseUrl, log) {
79
+ try {
80
+ const res = await fetch(`${apiBaseUrl}/location/getIpLocation`, {
81
+ headers: { Accept: "application/json" },
82
+ });
83
+ if (!res.ok) { return null; }
84
+ const data = await res.json();
85
+ if (!data?.success || !data?.location) { return null; }
86
+ const coords = normalizeCoords(data.location.latitude, data.location.longitude);
87
+ if (!coords) { return null; }
88
+ return { ...data.location, ...coords, source: "ip_location", priority: PRIORITY.IP };
89
+ } catch (err) {
90
+ log.warn("IP location fetch failed", { message: err?.message });
91
+ return null;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Resolve the best available location.
97
+ * @param {{ userUid?: string, apiBaseUrl?: string, useDeviceLocation?: boolean, useIpFallback?: boolean, logger?: object }} opts
98
+ */
99
+ export async function resolveLocation({
100
+ userUid,
101
+ apiBaseUrl = FAPK_API,
102
+ useDeviceLocation = false,
103
+ useIpFallback = false,
104
+ logger,
105
+ } = {}) {
106
+ const log = ensureLog(logger);
107
+
108
+ if (userUid) {
109
+ const dbLocation = await fetchUserDatabaseLocation(userUid, apiBaseUrl, log);
110
+ if (dbLocation) { return dbLocation; }
111
+ log.warn("User DB location unavailable", { uid: userUid });
112
+ return null;
113
+ }
114
+
115
+ if (useDeviceLocation) {
116
+ const deviceLocation = await fetchDeviceLocation(log);
117
+ if (deviceLocation) { return deviceLocation; }
118
+ }
119
+
120
+ if (useIpFallback) {
121
+ const ipLocation = await fetchIpLocation(apiBaseUrl, log);
122
+ if (ipLocation) { return ipLocation; }
123
+ }
124
+
125
+ log.warn("All location sources exhausted");
126
+ return null;
127
+ }
128
+
129
+ export default resolveLocation;
@@ -1,7 +1,7 @@
1
1
  // packages/sanvika-geolocation-sdk/src/server/serviceClient.js
2
2
  // HTTP client for geolocation.sanvikaproduction.com — used by all server-side helpers.
3
3
  // Universal S2S Auth Pattern (ecosystem rule §21):
4
- // PROJECT_NAME — clientId
4
+ // CLIENT_ID — clientId
5
5
  // SANVIKA_SERVICE_KEY — Tier 1 trust
6
6
  // GEOLOCATION_URL (or GEO_URL) — service endpoint
7
7
  // GEOLOCATION_CLIENT_SECRET — (optional, Tier 2 fallback for 3rd-party)
@@ -24,11 +24,11 @@ export class GeolocationServiceClient {
24
24
 
25
25
  constructor({ url, clientId, serviceKey, secret } = {}) {
26
26
  const finalUrl = url || readEnv("GEOLOCATION_URL") || readEnv("GEO_URL");
27
- const finalClientId = clientId || readEnv("PROJECT_NAME") || readEnv("GEOLOCATION_CLIENT_ID");
27
+ const finalClientId = clientId || readEnv("CLIENT_ID") || readEnv("GEOLOCATION_CLIENT_ID");
28
28
  const finalServiceKey = serviceKey || readEnv("SANVIKA_SERVICE_KEY") || "";
29
29
  const finalSecret = secret || readEnv("GEOLOCATION_CLIENT_SECRET") || ""; // Tier 2 fallback (optional)
30
30
  if (!finalUrl || !finalClientId) {
31
- throw new Error("@sanvika/geolocation/server: GEOLOCATION_URL and PROJECT_NAME are required");
31
+ throw new Error("@sanvika/geolocation/server: GEOLOCATION_URL and CLIENT_ID are required");
32
32
  }
33
33
  this.#url = finalUrl.replace(/\/$/, "");
34
34
  this.#clientId = finalClientId;