diff --git a/src/api/fetch.ts b/src/api/fetch.ts new file mode 100644 index 0000000..215ba98 --- /dev/null +++ b/src/api/fetch.ts @@ -0,0 +1,41 @@ +/** + * Fetches a response from the Incentives API. Handles turning an error response + * into an exception with a useful message. + */ +export async function fetchApi( + apiKey: string, + apiHost: string, + path: string, + query: URLSearchParams, +) { + const url = new URL(apiHost); + url.pathname = path; + url.search = query.toString(); + const response: Response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); + if (response.status >= 400) { + console.error(response); + // statusText isn't always set, but it's a reasonable proxy for a human readable error if it is: + let message = response.statusText; + try { + const error = await response.json(); + console.error(error); + if (error.title && error.detail) { + // Zuplo's API key errors have this form: + message = `${error.title}: ${error.detail}`; + } else if (error.message && error.error) { + // Rewiring America's API errors have this form: + message = `${error.error}: ${error.message}`; + } + } catch (e) { + // if we couldn't get anything off the response, just go with something generic: + message = 'Error loading incentives.'; + } + throw new Error(message); + } + return response.json(); +} diff --git a/src/calculator-footer.ts b/src/calculator-footer.ts new file mode 100644 index 0000000..f72d116 --- /dev/null +++ b/src/calculator-footer.ts @@ -0,0 +1,21 @@ +import { html } from 'lit'; + +export const CALCULATOR_FOOTER = html``; diff --git a/src/calculator.ts b/src/calculator.ts index 3f8803b..b8c80da 100644 --- a/src/calculator.ts +++ b/src/calculator.ts @@ -10,6 +10,8 @@ import { ICalculatedIncentiveResults, OwnerStatus, } from './calculator-types'; +import { CALCULATOR_FOOTER } from './calculator-footer'; +import { fetchApi } from './api/fetch'; const loadedTemplate = ( results: ICalculatedIncentiveResults, @@ -32,8 +34,7 @@ const errorTemplate = (error: unknown) => html` `; -const DEFAULT_CALCULATOR_API_PATH: string = - 'https://api.rewiringamerica.org/api/v0/calculator'; +const DEFAULT_CALCULATOR_API_HOST: string = 'https://api.rewiringamerica.org'; @customElement('rewiring-america-calculator') export class RewiringAmericaCalculator extends LitElement { @@ -62,8 +63,8 @@ export class RewiringAmericaCalculator extends LitElement { @property({ type: String, attribute: 'api-key' }) apiKey: string = ''; - @property({ type: String, attribute: 'api-path' }) - apiPath: string = DEFAULT_CALCULATOR_API_PATH; + @property({ type: String, attribute: 'api-host' }) + apiHost: string = DEFAULT_CALCULATOR_API_HOST; /* supported properties to allow pre-filling the form */ @@ -123,34 +124,12 @@ export class RewiringAmericaCalculator extends LitElement { tax_filing, household_size, }); - const url = new URL(`${this.apiPath}?${query}`); - const response: Response = await fetch(url, { - method: 'GET', - headers: { - Authorization: `Bearer ${this.apiKey}`, - }, - }); - if (response.status >= 400) { - console.error(response); - // statusText isn't always set, but it's a reasonable proxy for a human readable error if it is: - let message = response.statusText; - try { - const error = await response.json(); - console.error(error); - if (error.title && error.detail) { - // Zuplo's API key errors have this form: - message = `${error.title}: ${error.detail}`; - } else if (error.message && error.error) { - // Rewiring America's API errors have this form: - message = `${error.error}: ${error.message}`; - } - } catch (e) { - // if we couldn't get anything off the response, just go with something generic: - message = 'Error loading incentives.'; - } - throw new Error(message); - } - return response.json(); + return await fetchApi( + this.apiKey, + this.apiHost, + '/api/v0/calculator', + query, + ); }, // if the args array changes then the task will run again args: () => [ @@ -187,27 +166,7 @@ export class RewiringAmericaCalculator extends LitElement { error: errorTemplate, })} `} - + ${CALCULATOR_FOOTER} `; }