-
Notifications
You must be signed in to change notification settings - Fork 394
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[google-maps-input] Catch authentication errors when loading
- Loading branch information
Showing
5 changed files
with
185 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 30 additions & 34 deletions
64
packages/@sanity/google-maps-input/src/loader/GoogleMapsLoadProxy.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,53 +1,49 @@ | ||
import React from 'react' | ||
import {loadGoogleMapsApi} from './loadGoogleMapsApi' | ||
import {Subscription} from 'rxjs' | ||
import {loadGoogleMapsApi, GoogleLoadState} from './loadGoogleMapsApi' | ||
import {LoadError} from './LoadError' | ||
|
||
interface LoadProps { | ||
children: (api: typeof window.google.maps) => React.ReactElement | ||
} | ||
|
||
interface LoadState { | ||
loading: boolean | ||
error?: Error | ||
api?: typeof window.google.maps | ||
} | ||
export class GoogleMapsLoadProxy extends React.Component<LoadProps, GoogleLoadState> { | ||
loadSubscription: Subscription | undefined | ||
|
||
export class GoogleMapsLoadProxy extends React.Component<LoadProps, LoadState> { | ||
constructor(props: LoadProps) { | ||
super(props) | ||
|
||
const api = | ||
typeof window !== 'undefined' && window.google && window.google.maps | ||
? window.google.maps | ||
: undefined | ||
|
||
this.state = {loading: !api, api} | ||
this.state = {loadState: 'loading'} | ||
|
||
let sync = true | ||
this.loadSubscription = loadGoogleMapsApi().subscribe(loadState => { | ||
if (sync) { | ||
this.state = loadState | ||
} else { | ||
this.setState(loadState) | ||
} | ||
}) | ||
sync = false | ||
} | ||
|
||
componentDidMount() { | ||
if (this.state.api) { | ||
// Already loaded | ||
return | ||
componentWillUnmount() { | ||
if (this.loadSubscription) { | ||
this.loadSubscription.unsubscribe() | ||
} | ||
|
||
loadGoogleMapsApi() | ||
.then(api => this.setState({loading: false, api})) | ||
.catch(error => this.setState({error})) | ||
} | ||
|
||
render() { | ||
const {error, loading, api} = this.state | ||
if (error) { | ||
return <div>Load error: {error.stack}</div> | ||
} | ||
|
||
if (loading) { | ||
return <div>Loading Google Maps API</div> | ||
} | ||
|
||
if (api) { | ||
return this.props.children(api) || null | ||
switch (this.state.loadState) { | ||
case 'loadError': | ||
return <LoadError error={this.state.error} isAuthError={false} /> | ||
case 'authError': | ||
return <LoadError isAuthError /> | ||
case 'loading': | ||
return <div>Loading Google Maps API</div> | ||
case 'loaded': | ||
return this.props.children(this.state.api) || null | ||
default: | ||
return null | ||
} | ||
|
||
return null | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
packages/@sanity/google-maps-input/src/loader/LoadError.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
@import 'part:@sanity/base/theme/variables-style'; | ||
|
||
.card { | ||
max-width: 720px; | ||
margin: 0 auto; | ||
} | ||
|
||
.cardHeader { | ||
padding: var(--medium-padding); | ||
} | ||
|
||
.cardTitle { | ||
composes: heading2 from 'part:@sanity/base/theme/typography/headings-style'; | ||
text-align: center; | ||
} | ||
|
||
.cardContent { | ||
padding: var(--medium-padding); | ||
} | ||
|
||
.stack { | ||
max-width: 720px; | ||
margin: 0 auto; | ||
|
||
@nest & pre { | ||
border: 1px solid #ccc; | ||
padding: var(--medium-padding); | ||
background: var(--pre-bg); | ||
color: var(--pre-color); | ||
font-family: var(--font-family-monospace); | ||
font-size: var(--font-size-base); | ||
line-height: var(--line-height-base); | ||
overflow: auto; | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
packages/@sanity/google-maps-input/src/loader/LoadError.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import * as React from 'react' | ||
import styles from './LoadError.css' | ||
|
||
type Props = {error: Error; isAuthError: false} | {isAuthError: true} | ||
|
||
export function LoadError(props: Props) { | ||
return ( | ||
<div className={styles.card}> | ||
<header className={styles.cardHeader}> | ||
<h2 className={styles.cardTitle}>Google Maps failed to load</h2> | ||
</header> | ||
|
||
<div className={styles.cardContent}> | ||
{props.isAuthError ? ( | ||
<AuthError /> | ||
) : ( | ||
<> | ||
<h3>Error details:</h3> | ||
<pre> | ||
<code>{props.error.message}</code> | ||
</pre> | ||
</> | ||
)} | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
function AuthError() { | ||
return ( | ||
<> | ||
<p>The error appears to be related to authentication</p> | ||
<p>Common causes include:</p> | ||
<ul> | ||
<li>Incorrect API key</li> | ||
<li>Referer not allowed</li> | ||
<li>Missing authentication scope</li> | ||
</ul> | ||
<p>Check the browser developer tools for more information.</p> | ||
</> | ||
) | ||
} |
94 changes: 76 additions & 18 deletions
94
packages/@sanity/google-maps-input/src/loader/loadGoogleMapsApi.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,90 @@ | ||
import {Observable, BehaviorSubject} from 'rxjs' | ||
import config from 'config:@sanity/google-maps-input' | ||
|
||
const callbackName = '___sanity_googleMapsApiCallback' | ||
const authFailureCallbackName = 'gm_authFailure' | ||
const locale = (typeof window !== 'undefined' && window.navigator.language) || 'en' | ||
|
||
let loadingPromise: Promise<typeof window.google.maps> | ||
export interface LoadingState { | ||
loadState: 'loading' | ||
} | ||
|
||
export interface LoadedState { | ||
loadState: 'loaded' | ||
api: typeof window.google.maps | ||
} | ||
|
||
export interface LoadErrorState { | ||
loadState: 'loadError' | ||
error: Error | ||
} | ||
|
||
export interface AuthErrorState { | ||
loadState: 'authError' | ||
} | ||
|
||
export type GoogleLoadState = LoadingState | LoadedState | LoadErrorState | AuthErrorState | ||
|
||
let subject: BehaviorSubject<GoogleLoadState> | ||
|
||
export function loadGoogleMapsApi(): Promise<typeof window.google.maps> { | ||
const callbackName = '___sanity_googleMapsApiCallback' | ||
export function loadGoogleMapsApi(): Observable<GoogleLoadState> { | ||
const selectedLocale = locale || 'en-US' | ||
|
||
if (window.google && window.google.maps) { | ||
return Promise.resolve(window.google.maps) | ||
if (subject) { | ||
return subject | ||
} | ||
|
||
subject = new BehaviorSubject<GoogleLoadState>({loadState: 'loading'}) | ||
|
||
window[authFailureCallbackName] = () => { | ||
delete window[authFailureCallbackName] | ||
subject.next({loadState: 'authError'}) | ||
} | ||
|
||
if (window[callbackName]) { | ||
return loadingPromise | ||
window[callbackName] = () => { | ||
delete window[callbackName] | ||
subject.next({loadState: 'loaded', api: window.google.maps}) | ||
} | ||
|
||
loadingPromise = new Promise((resolve, reject) => { | ||
window[callbackName] = () => { | ||
delete window[callbackName] | ||
resolve(window.google.maps) | ||
} | ||
const script = document.createElement('script') | ||
script.onerror = ( | ||
event: Event | string, | ||
source?: string, | ||
lineno?: number, | ||
colno?: number, | ||
error?: Error | ||
) => | ||
subject.next({ | ||
loadState: 'loadError', | ||
error: coeerceError(event, error) | ||
} as LoadErrorState) | ||
|
||
const script = document.createElement('script') | ||
script.onerror = reject | ||
script.src = `https://maps.googleapis.com/maps/api/js?key=${config.apiKey}&libraries=places&callback=${callbackName}&language=${selectedLocale}` | ||
document.getElementsByTagName('head')[0].appendChild(script) | ||
}) | ||
script.src = `https://maps.googleapis.com/maps/api/js?key=${config.apiKey}&libraries=places&callback=${callbackName}&language=${selectedLocale}` | ||
document.getElementsByTagName('head')[0].appendChild(script) | ||
|
||
return subject | ||
} | ||
|
||
function coeerceError(event: Event | string, error?: Error): Error { | ||
if (error) { | ||
return error | ||
} | ||
|
||
if (typeof event === 'string') { | ||
return new Error(event) | ||
} | ||
|
||
return new Error(isErrorEvent(event) ? event.message : 'Failed to load Google Maps API') | ||
} | ||
|
||
function isErrorEvent(event: unknown): event is ErrorEvent { | ||
if (typeof event !== 'object' || event === null) { | ||
return false | ||
} | ||
|
||
if (!('message' in event)) { | ||
return false | ||
} | ||
|
||
return loadingPromise | ||
return typeof (event as ErrorEvent).message === 'string' | ||
} |