@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.
|
|
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/
|
|
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
|
+
}
|
package/src/client/index.js
CHANGED
|
@@ -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
|
-
//
|
|
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.
|
|
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 = (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
//
|
|
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("
|
|
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
|
|
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;
|