Skip to content

Commit

Permalink
[google-maps-input] Catch authentication errors when loading
Browse files Browse the repository at this point in the history
  • Loading branch information
rexxars committed Oct 6, 2020
1 parent 350b877 commit 780d007
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 53 deletions.
3 changes: 2 additions & 1 deletion packages/@sanity/google-maps-input/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"sanity-plugin"
],
"dependencies": {
"@sanity/react-hooks": "1.150.4"
"@sanity/react-hooks": "1.150.4",
"rxjs": "^6.5.3"
},
"devDependencies": {
"@sanity/base": "1.150.3",
Expand Down
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 packages/@sanity/google-maps-input/src/loader/LoadError.css
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 packages/@sanity/google-maps-input/src/loader/LoadError.tsx
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 packages/@sanity/google-maps-input/src/loader/loadGoogleMapsApi.ts
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'
}

0 comments on commit 780d007

Please sign in to comment.