Skip to content

Instantly share code, notes, and snippets.

@littlemooon
Created November 24, 2018 09:55
Show Gist options
  • Save littlemooon/1e63fcd7510e62b0eb44ea84b388aac5 to your computer and use it in GitHub Desktop.
Save littlemooon/1e63fcd7510e62b0eb44ea84b388aac5 to your computer and use it in GitHub Desktop.
Country context state component
import to from 'await-to-js';
import countries from 'common/config/world-countries.js';
import { waitUntilResolved } from 'common/lib/fetch-helpers';
import { formatOperatorResponse } from 'common/lib/operator';
import {
getStorageItem,
removeStorageItem,
setStorageItem,
StorageKey,
} from 'common/lib/storage';
import { Country } from 'common/types';
import * as React from 'react';
import { apiClient } from '../lib/api-client';
export enum CountryStateStatus {
LOADING = 'LOADING',
PARTIAL = 'PARTIAL',
SUCCESS = 'SUCCESS',
ERROR = 'ERROR',
}
export interface CountryState {
country?: Country;
isCountryRoute: boolean;
shouldStore: boolean;
error?: Error;
status: CountryStateStatus;
setCountry: (c?: Country) => void;
}
interface CountryStateProps {
pathLast?: string;
children: (props: { countryState: CountryState }) => React.ReactNode;
}
const noop = () => {};
const defaultState: CountryState = {
status: CountryStateStatus.LOADING,
isCountryRoute: false,
shouldStore: true,
setCountry: noop,
};
export const CountryStateContext = React.createContext<CountryState>(
defaultState
);
function isValidCountry(country?: Country) {
return (
!country ||
Boolean(countries.find((c: Country) => c.alpha2 === country.alpha2))
);
}
function hasChanged(prop: string, oldCountry?: Country, newCountry?: Country) {
return (newCountry && newCountry[prop]) !== (oldCountry && oldCountry[prop]);
}
export class CountryStateProvider extends React.Component<
CountryStateProps,
CountryState
> {
state = defaultState;
fetchCountryLookup = waitUntilResolved<void>(async () => {
const [error, countryRes] = await to<Country | undefined, Error>(
apiClient.fetch('/lookup_country', {})
);
if (error) {
this.setCountry(countries.find(c => c.alpha2 === 'XI'));
} else if (countryRes && countryRes.alpha2) {
this.setCountry(countryRes);
}
});
fetchCountryProducts = waitUntilResolved<Country | undefined>(
async country => {
const query =
country && country.alpha2 !== 'XI'
? { country: country.alpha2 }
: undefined;
const [error, countryRes] = await to<Country | undefined, Error>(
apiClient.fetch('/accounts/products', { query })
);
if (error) {
this.setState({
error,
status: CountryStateStatus.ERROR,
});
} else if (countryRes && countryRes.alpha2) {
const { operators = [], popular = [], name = '' } = countryRes;
this.setCountry(
{
...countryRes,
operators: operators.map(o => formatOperatorResponse(o, name)),
popular: popular.map(o => formatOperatorResponse(o)),
},
{
status: CountryStateStatus.SUCCESS,
}
);
}
}
);
componentDidMount = () => {
this.getInitialCountry();
};
getInitialCountry() {
const { pathLast } = this.props;
const storedCountry = getStorageItem(StorageKey.STATE_COUNTRY);
const pathCountry = countries.find(
(c: Country) => c.name.toLowerCase().replace(/ /g, '-') === pathLast
);
if (pathCountry) {
this.setCountry(pathCountry, {
isCountryRoute: true,
shouldStore: false,
});
} else if (storedCountry && isValidCountry(storedCountry)) {
this.setCountry(storedCountry);
} else {
this.fetchCountryLookup();
}
}
setCountry = (
newCountry?: Country,
otherState: Partial<CountryState> = {}
) => {
const { country } = this.state;
if (
hasChanged('alpha2', country, newCountry) ||
hasChanged('slug', country, newCountry) ||
Boolean(Object.keys(otherState).length)
) {
this.setState(
state => {
const newState: CountryState = {
...state,
country: newCountry,
...otherState,
};
if (newState.shouldStore) {
if (newCountry) {
setStorageItem(StorageKey.STATE_COUNTRY, newCountry);
} else {
removeStorageItem(StorageKey.STATE_COUNTRY);
}
}
return newState;
},
() => this.fetchCountryProducts(newCountry)
);
}
};
render() {
const countryState = {
...this.state,
setCountry: this.setCountry,
};
return (
<CountryStateContext.Provider value={countryState}>
{this.props.children({ countryState })}
</CountryStateContext.Provider>
);
}
}
export const withCountryState = <P extends object>(
Component: React.ComponentType<P & { countryState: CountryState }>
) =>
class WithCountryState extends React.Component<P> {
render() {
return (
<CountryStateContext.Consumer>
{(countryState: CountryState) => (
<Component countryState={countryState} {...this.props} />
)}
</CountryStateContext.Consumer>
);
}
};
export function waitUntilResolved<T>(fn: (arg: T) => Promise<any>) {
let waiting = false;
return async (arg: T) => {
if (waiting) {
return;
}
waiting = true;
await fn(arg);
waiting = false;
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment