Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 93 additions & 3 deletions packages/components/built/UtilsContext.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,49 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { createContext, useContext, useState } from 'react';
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { createContext, useContext, useEffect, useState } from 'react';
import { SnackBar } from './SnackBar';
import { Loader } from './Loader';
import { checkGeoLocation } from './utils/geolocation';
import { Modal } from './Modal';
var errorGeolocationModal = 'error-geolocation';
function ErrorContent(msg) {
return (_jsx(_Fragment, { children: _jsx("div", { className: "p-4 md:p-5 dark:text-gray-400", children: _jsxs("div", { children: ["The app is not accessible from your location.", _jsx("br", {}), _jsx("br", {}), msg] }) }) }));
}
var utilsContext = createContext(undefined);
export var useUtilsComponents = function () {
var context = useContext(utilsContext);
Expand All @@ -10,6 +52,54 @@ export var useUtilsComponents = function () {
}
return context;
};
// GeoLocationWrapper Component
function GeoLocationWrapper(_a) {
var _this = this;
var children = _a.children;
var _b = useState(null), isValidLocation = _b[0], setIsValidLocation = _b[1];
var showLoader = useUtilsComponents().showLoader;
var isEnabled = import.meta.env.VITE_ENABLE_GEOLOCATION === 'true';
useEffect(function () {
var checkLocation = function () { return __awaiter(_this, void 0, void 0, function () {
var isValid, error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!isEnabled) {
setIsValidLocation(true);
return [2 /*return*/];
}
_a.label = 1;
case 1:
_a.trys.push([1, 3, 4, 5]);
showLoader(true);
return [4 /*yield*/, checkGeoLocation()];
case 2:
isValid = _a.sent();
setIsValidLocation(isValid);
return [3 /*break*/, 5];
case 3:
error_1 = _a.sent();
setIsValidLocation(false);
return [3 /*break*/, 5];
case 4:
showLoader(false);
return [7 /*endfinally*/];
case 5: return [2 /*return*/];
}
});
}); };
checkLocation();
}, []);
if (isEnabled && isValidLocation === null) {
return null;
}
if (isEnabled && isValidLocation === false) {
Modal.showModal(errorGeolocationModal);
return null;
}
return _jsx(_Fragment, { children: children });
}
export var UtilsProvider = function (_a) {
var children = _a.children;
var _b = useState(null), snackBar = _b[0], setSnackBar = _b[1];
Expand All @@ -23,7 +113,7 @@ export var UtilsProvider = function (_a) {
var hideSnackBar = function () {
setSnackBar(null);
};
return (_jsxs(utilsContext.Provider, { value: { showSnackBar: showSnackBar, hideSnackBar: hideSnackBar, showLoader: showLoader }, children: [children, snackBar && (_jsx(SnackBar, { message: snackBar.message, success: snackBar.success, hideSnackBar: hideSnackBar })), isLoading && _jsx(Loader, {})] }));
return (_jsxs(utilsContext.Provider, { value: { showSnackBar: showSnackBar, hideSnackBar: hideSnackBar, showLoader: showLoader }, children: [_jsx(GeoLocationWrapper, { children: children }), snackBar && (_jsx(SnackBar, { message: snackBar.message, success: snackBar.success, hideSnackBar: hideSnackBar })), isLoading && _jsx(Loader, {}), _jsx(Modal.Component, { title: 'Access Denied', content: ErrorContent, id: errorGeolocationModal })] }));
};
export var UtilsContext = {
UtilsProvider: UtilsProvider,
Expand Down
54 changes: 54 additions & 0 deletions packages/components/built/geodata/us-states.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/components/built/utils/geolocation.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export declare const checkGeoLocation: () => Promise<boolean>;
92 changes: 92 additions & 0 deletions packages/components/built/utils/geolocation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
import * as turf from '@turf/turf';
import usStatesJson from '../geodata/us-states.json' assert { type: 'json' };
var usStates = usStatesJson;
function getStatesContainingPoint(point, geojsonData) {
var matchingStates = [];
geojsonData.features.forEach(function (state) {
if (turf.booleanPointInPolygon(point, state.geometry)) {
var stateName = state.properties.name;
matchingStates.push(stateName);
}
});
return matchingStates;
}
export var checkGeoLocation = function () { return __awaiter(void 0, void 0, void 0, function () {
var isEnabled, blockedStates;
var _a;
return __generator(this, function (_b) {
isEnabled = import.meta.env.VITE_ENABLE_GEOLOCATION === 'true';
blockedStates = ((_a = import.meta.env.VITE_BLOCKED_STATES) === null || _a === void 0 ? void 0 : _a.split(',')) || [];
if (!isEnabled)
return [2 /*return*/, true]; // Skip geolocation checks if not enabled
return [2 /*return*/, new Promise(function (resolve, reject) {
if (!navigator.geolocation) {
reject(new Error('Geolocation is not supported by your browser.'));
return;
}
navigator.geolocation.getCurrentPosition(function (position) { return __awaiter(void 0, void 0, void 0, function () {
var _a, latitude, longitude, point, statesContainingPoint;
return __generator(this, function (_b) {
try {
_a = position.coords, latitude = _a.latitude, longitude = _a.longitude;
point = turf.point([longitude, latitude]);
statesContainingPoint = getStatesContainingPoint(point.geometry, usStates);
if (statesContainingPoint.length > 1) {
// The getStatesContainingPoint returned multiple points this is not expected
resolve(false);
}
else if (statesContainingPoint[0] && blockedStates.includes(statesContainingPoint[0])) {
// The user is accessing from one of the blocked US states
resolve(false);
}
else {
resolve(true);
}
}
catch (error) {
reject(new Error('Failed to determine your location. Please try again.'));
}
return [2 /*return*/];
});
}); }, function () {
reject(new Error('You denied location access. Location is mandatory for legal reasons.'));
});
})];
});
}); };
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"dependencies": {
"@bitcoin-computer/lib": "^0.24.3-beta.0",
"@turf/turf": "^7.2.0",
"flowbite": "^2.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
65 changes: 62 additions & 3 deletions packages/components/src/UtilsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
import React, { createContext, ReactNode, useContext, useState } from 'react'
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react'
import { SnackBar } from './SnackBar'
import { Loader } from './Loader'
import { checkGeoLocation } from './utils/geolocation'
import { Modal } from './Modal'

const errorGeolocationModal = 'error-geolocation'

function ErrorContent(msg: string) {
return (
<>
<div className="p-4 md:p-5 dark:text-gray-400">
<div>
The app is not accessible from your location.
<br />
<br />
{msg}
</div>
</div>
</>
)
}

interface UtilsContextProps {
showSnackBar: (message: string, success: boolean) => void
Expand All @@ -18,8 +37,47 @@ export const useUtilsComponents = (): UtilsContextProps => {
return context
}

// GeoLocationWrapper Component
function GeoLocationWrapper({ children }: { children: React.ReactNode }) {
const [isValidLocation, setIsValidLocation] = useState<boolean | null>(null)
const { showLoader } = useUtilsComponents()
const isEnabled = import.meta.env.VITE_ENABLE_GEOLOCATION === 'true'

useEffect(() => {
const checkLocation = async () => {
if (!isEnabled) {
setIsValidLocation(true)
return
}

try {
showLoader(true)
const isValid = await checkGeoLocation()
setIsValidLocation(isValid)
} catch (error) {
setIsValidLocation(false)
} finally {
showLoader(false)
}
}

checkLocation()
}, [])

if (isEnabled && isValidLocation === null) {
return null
}

if (isEnabled && isValidLocation === false) {
Modal.showModal(errorGeolocationModal)
return null
}

return <>{children}</>
}

interface UtilsProviderProps {
children: ReactNode // Explicitly type children as ReactNode
children: ReactNode
}

export const UtilsProvider: React.FC<UtilsProviderProps> = ({ children }) => {
Expand All @@ -40,7 +98,7 @@ export const UtilsProvider: React.FC<UtilsProviderProps> = ({ children }) => {

return (
<utilsContext.Provider value={{ showSnackBar, hideSnackBar, showLoader }}>
{children}
<GeoLocationWrapper>{children}</GeoLocationWrapper>
{snackBar && (
<SnackBar
message={snackBar.message}
Expand All @@ -49,6 +107,7 @@ export const UtilsProvider: React.FC<UtilsProviderProps> = ({ children }) => {
/>
)}
{isLoading && <Loader />}
<Modal.Component title={'Access Denied'} content={ErrorContent} id={errorGeolocationModal} />
</utilsContext.Provider>
)
}
Expand Down
54 changes: 54 additions & 0 deletions packages/components/src/geodata/us-states.json

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions packages/components/src/utils/geolocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as turf from '@turf/turf'
import usStatesJson from '../geodata/us-states.json' assert { type: 'json' }

interface StateProperties {
name: string
density: number
}
type StateFeature = GeoJSON.Feature<GeoJSON.Polygon | GeoJSON.MultiPolygon, StateProperties>
type StatesGeoJSON = GeoJSON.FeatureCollection<
GeoJSON.Polygon | GeoJSON.MultiPolygon,
StateProperties
>

const usStates: StatesGeoJSON = usStatesJson as StatesGeoJSON

function getStatesContainingPoint(point: GeoJSON.Point, geojsonData: StatesGeoJSON): string[] {
const matchingStates: string[] = []

geojsonData.features.forEach((state: StateFeature) => {
if (turf.booleanPointInPolygon(point, state.geometry)) {
const stateName = state.properties.name
matchingStates.push(stateName)
}
})

return matchingStates
}

export const checkGeoLocation = async (): Promise<boolean> => {
const isEnabled = import.meta.env.VITE_ENABLE_GEOLOCATION === 'true'
const blockedStates: string[] = import.meta.env.VITE_BLOCKED_STATES?.split(',') || []

if (!isEnabled) return true // Skip geolocation checks if not enabled

return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('Geolocation is not supported by your browser.'))
return
}

navigator.geolocation.getCurrentPosition(
async (position) => {
try {
const { latitude, longitude } = position.coords

const point = turf.point([longitude, latitude])

const statesContainingPoint = getStatesContainingPoint(point.geometry, usStates)
if (statesContainingPoint.length > 1) {
// The getStatesContainingPoint returned multiple points this is not expected
resolve(false)
} else if (statesContainingPoint[0] && blockedStates.includes(statesContainingPoint[0])) {
// The user is accessing from one of the blocked US states
resolve(false)
} else {
resolve(true)
}
} catch (error) {
reject(new Error('Failed to determine your location. Please try again.'))
}
},
() => {
reject(new Error('You denied location access. Location is mandatory for legal reasons.'))
},
)
})
}
2 changes: 2 additions & 0 deletions packages/vite-template/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ VITE_PORT=1032
# Smart Contract Locations
# Run 'npm run deploy' and copy the output here
VITE_COUNTER_MOD_SPEC=fddcf4fe11f7460ea2efa2912cb3feee73773beffcd9216e6bd6b28ad9c59518:0
VITE_ENABLE_GEOLOCATION=true
VITE_BLOCKED_STATES=California
7 changes: 7 additions & 0 deletions packages/vite-template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ If you want to add a feature we recommend to create a fork. Let us know if you h

See [here](https://github.com/bitcoin-computer/monorepo/tree/main/packages/lib#legal-notice).

## Enable GeoLocation based blocking.

We allow basic geolocation based blocking in-built in the app, you can block access to the US states with following configuration

- VITE_ENABLE_GEOLOCATION (default false) should be set to true
- VITE_BLOCKED_STATES (default []) should be set to , separated string e.g. California,texas

## MIT License

Copyright (c) 2022 BCDB Inc.
Expand Down