-
Notifications
You must be signed in to change notification settings - Fork 402
fix(checkout): CHECKOUT-10026 Add new Places API support for Google Autocomplete with legacy fallback #3110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
588c55f
fix(checkout): CHECKOUT-10026 migrate Google Places script loader to …
bc-maxy bd75dcc
fix(checkout): CHECKOUT-10026 Migrate GoogleAutocompleteService
bc-maxy a9876e5
fix(checkout): CHECKOUT-10026 Update GoogleAutocomplete file
bc-maxy 1322a5a
fix(checkout): CHECKOUT-10026 Various quick fixes
bc-maxy f7af121
fix(checkout): CHECKOUT-10026 Resolve cursor comments
bc-maxy a5d1b4b
fix(checkout): CHECKOUT-10026 Add session token to suggestion calls
bc-maxy e02bfce
fix(checkout): CHECKOUT-10026 Various small cleanup
bc-maxy b93d093
fix(checkout): CHECKOUT-10026 Keep the old service alive
bc-maxy 491717a
chore(checkout): CHECKOUT-10026 Clean up the code
bc-maxy dd051c8
feat(checkout): CHECKOUT-10026 Fall back to new service if old one fails
bc-maxy 0e95de2
fix(checkout): CHECKOUT-10026 Adjust functions to work correctly
bc-maxy 9389b14
fix(checkout): CHECKOUT-10026 Use module scoped variable
bc-maxy 9672f94
fix(checkout): CHECKOUT-10026 Fix review comments
bc-maxy 9796c11
fix(checkout): CHECKOUT-10026 Clean up the implementation
bc-maxy acad608
fix(checkout): CHECKOUT-10026 Update test file path
bc-maxy 164211b
fix(checkout): CHECKOUT-10026 Try new service first, fall back to old
bc-maxy 34537f2
fix(checkout): CHECKOUT-10026 Clean up the implementation
bc-maxy bcc8a31
fix(checkout): CHECKOUT-10026 A small refactor
bc-maxy 974d130
chore(checkout): CHECKOUT-10026 Fix typos and unused exports
bc-maxy 45efb30
fix(checkout): CHECKOUT-10026 Fall back to legacy only for permission…
bc-maxy 9648939
fix(checkout): CHECKOUT-10026 Hide behind feature flag WIP
bc-maxy 08fc93e
Merge branch 'master' into checkout-10026
bc-maxy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or 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
65 changes: 16 additions & 49 deletions
65
packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteScriptLoader.ts
This file contains hidden or 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,59 +1,26 @@ | ||
| import { getScriptLoader, type ScriptLoader } from '@bigcommerce/script-loader'; | ||
|
|
||
| import { type GoogleAutocompleteWindow, type GoogleMapsSdk } from './googleAutocompleteTypes'; | ||
| export class GoogleAutocompleteScriptLoader { | ||
| private _scriptLoader: ScriptLoader = getScriptLoader(); | ||
| private _placesPromise?: Promise<google.maps.PlacesLibrary>; | ||
|
|
||
| export default class GoogleAutocompleteScriptLoader { | ||
| private _scriptLoader: ScriptLoader; | ||
| private _googleAutoComplete?: Promise<GoogleMapsSdk>; | ||
|
|
||
| constructor() { | ||
| this._scriptLoader = getScriptLoader(); | ||
| } | ||
|
|
||
| loadMapsSdk(apiKey: string): Promise<GoogleMapsSdk> { | ||
| if (this._googleAutoComplete) { | ||
| return this._googleAutoComplete; | ||
| loadPlacesLibrary(apiKey: string): Promise<google.maps.PlacesLibrary> { | ||
| if (this._placesPromise) { | ||
| return this._placesPromise; | ||
| } | ||
|
|
||
| this._googleAutoComplete = new Promise((resolve, reject) => { | ||
| const callbackName = 'initAutoComplete'; | ||
| const params = [ | ||
| 'language=en', | ||
| `key=${apiKey}`, | ||
| 'libraries=places', | ||
| `callback=${callbackName}`, | ||
| ].join('&'); | ||
| const params = ['language=en', `key=${apiKey}`, 'loading=async'].join('&'); | ||
|
|
||
| (window as GoogleCallbackWindow)[callbackName] = () => { | ||
| if (isAutocompleteWindow(window)) { | ||
| resolve(window.google.maps); | ||
| } | ||
| const promise = this._scriptLoader | ||
| .loadScript(`//maps.googleapis.com/maps/api/js?${params}`) | ||
| .then(() => google.maps.importLibrary('places') as Promise<google.maps.PlacesLibrary>) | ||
| .catch((e) => { | ||
| this._placesPromise = undefined; | ||
| throw e; | ||
| }); | ||
|
|
||
| reject(new Error('Failed to initialize Google Maps Autocomplete SDK.')); | ||
| }; | ||
| this._placesPromise = promise; | ||
|
|
||
| this._scriptLoader | ||
| .loadScript(`//maps.googleapis.com/maps/api/js?${params}`) | ||
| .catch((e) => { | ||
| this._googleAutoComplete = undefined; | ||
| throw e; | ||
| }); | ||
| }); | ||
|
|
||
| return this._googleAutoComplete; | ||
| return promise; | ||
| } | ||
| } | ||
|
|
||
| function isAutocompleteWindow(window: Window): window is GoogleAutocompleteWindow { | ||
| const autocompleteWindow = window as GoogleAutocompleteWindow; | ||
|
|
||
| return Boolean( | ||
| autocompleteWindow.google && | ||
| autocompleteWindow.google.maps && | ||
| autocompleteWindow.google.maps.places, | ||
| ); | ||
| } | ||
|
|
||
| export interface GoogleCallbackWindow extends Window { | ||
| initAutoComplete?(): void; | ||
| } |
96 changes: 96 additions & 0 deletions
96
packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteService.test.ts
This file contains hidden or 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,96 @@ | ||
| import { type GoogleAutocompleteScriptLoader } from './GoogleAutocompleteScriptLoader'; | ||
| import GoogleAutocompleteService from './GoogleAutocompleteService'; | ||
|
|
||
| const mockSuggestions = [ | ||
| { placePrediction: { placeId: 'place-1' } }, | ||
| ] as google.maps.places.AutocompleteSuggestion[]; | ||
|
|
||
| const mockPlace = { | ||
| fetchFields: jest.fn().mockResolvedValue(undefined), | ||
| addressComponents: [], | ||
| displayName: '123 Main St', | ||
| } as google.maps.places.Place; | ||
|
|
||
| const mockPlacesLibrary = { | ||
| AutocompleteSuggestion: { | ||
| fetchAutocompleteSuggestions: jest.fn().mockResolvedValue({ suggestions: mockSuggestions }), | ||
| }, | ||
| Place: jest.fn().mockImplementation(() => mockPlace), | ||
| } as google.maps.PlacesLibrary; | ||
|
|
||
| const mockScriptLoader = { | ||
| loadPlacesLibrary: jest.fn().mockResolvedValue(mockPlacesLibrary), | ||
| } as unknown as GoogleAutocompleteScriptLoader; | ||
|
|
||
| describe('GoogleAutocompleteService', () => { | ||
| let service: GoogleAutocompleteService; | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| service = new GoogleAutocompleteService('test-api-key', mockScriptLoader); | ||
| }); | ||
|
|
||
| describe('#getSuggestions()', () => { | ||
| it('returns suggestions from the API', async () => { | ||
| const result = await service.getSuggestions('123 Main', ['address']); | ||
|
|
||
| expect(result).toBe(mockSuggestions); | ||
| }); | ||
|
|
||
| it('passes input and types to fetchAutocompleteSuggestions', async () => { | ||
| await service.getSuggestions('123 Main', ['address']); | ||
|
|
||
| expect( | ||
| mockPlacesLibrary.AutocompleteSuggestion.fetchAutocompleteSuggestions, | ||
| ).toHaveBeenCalledWith( | ||
| expect.objectContaining({ input: '123 Main', includedPrimaryTypes: ['address'] }), | ||
| ); | ||
| }); | ||
|
|
||
| it('maps a string country restriction to includedRegionCodes array', async () => { | ||
| await service.getSuggestions('123 Main', ['address'], { country: 'US' }); | ||
|
|
||
| expect( | ||
| mockPlacesLibrary.AutocompleteSuggestion.fetchAutocompleteSuggestions, | ||
| ).toHaveBeenCalledWith(expect.objectContaining({ includedRegionCodes: ['US'] })); | ||
| }); | ||
|
|
||
| it('maps an array country restriction to includedRegionCodes', async () => { | ||
| await service.getSuggestions('123 Main', ['address'], { country: ['US', 'CA'] }); | ||
|
|
||
| expect( | ||
| mockPlacesLibrary.AutocompleteSuggestion.fetchAutocompleteSuggestions, | ||
| ).toHaveBeenCalledWith(expect.objectContaining({ includedRegionCodes: ['US', 'CA'] })); | ||
| }); | ||
|
|
||
| it('omits includedRegionCodes when no componentRestrictions provided', async () => { | ||
| await service.getSuggestions('123 Main', ['address']); | ||
|
|
||
| expect( | ||
| mockPlacesLibrary.AutocompleteSuggestion.fetchAutocompleteSuggestions, | ||
| ).toHaveBeenCalledWith(expect.objectContaining({ includedRegionCodes: undefined })); | ||
| }); | ||
| }); | ||
|
|
||
| describe('#getPlaceDetails()', () => { | ||
| it('constructs a Place with the given placeId', async () => { | ||
| await service.getPlaceDetails('place-1', ['addressComponents', 'displayName']); | ||
|
|
||
| expect(mockPlacesLibrary.Place).toHaveBeenCalledWith({ id: 'place-1' }); | ||
| }); | ||
|
|
||
| it('calls fetchFields with the given fields', async () => { | ||
| await service.getPlaceDetails('place-1', ['addressComponents', 'displayName']); | ||
|
|
||
| expect(mockPlace.fetchFields).toHaveBeenCalledWith({ | ||
| fields: ['addressComponents', 'displayName'], | ||
| }); | ||
| }); | ||
|
|
||
| it('returns the place after fetching fields', async () => { | ||
| const result = await service.getPlaceDetails('place-1', ['addressComponents']); | ||
|
|
||
| expect(result).toBe(mockPlace); | ||
| }); | ||
| }); | ||
| }); |
57 changes: 27 additions & 30 deletions
57
packages/core/src/app/address/googleAutocomplete/GoogleAutocompleteService.ts
This file contains hidden or 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,46 +1,43 @@ | ||
| import getGoogleAutocompleteScriptLoader from './getGoogleAutocompleteScriptLoader'; | ||
| import type GoogleAutocompleteScriptLoader from './GoogleAutocompleteScriptLoader'; | ||
| import { type GoogleAutocompleteScriptLoader } from './GoogleAutocompleteScriptLoader'; | ||
|
|
||
| export default class GoogleAutocompleteService { | ||
| private _autocompletePromise?: Promise<google.maps.places.AutocompleteService>; | ||
| private _placesPromise?: Promise<google.maps.places.PlacesService>; | ||
|
|
||
| constructor( | ||
| private _apiKey: string, | ||
| private _scriptLoader: GoogleAutocompleteScriptLoader = getGoogleAutocompleteScriptLoader(), | ||
| ) {} | ||
|
|
||
| getAutocompleteService(): Promise<google.maps.places.AutocompleteService> { | ||
| if (!this._autocompletePromise) { | ||
| this._autocompletePromise = this._scriptLoader | ||
| .loadMapsSdk(this._apiKey) | ||
| .then((googleMapsSdk) => { | ||
| if (!googleMapsSdk.places.AutocompleteService) { | ||
| throw new Error('`AutocompleteService` is undefined'); | ||
| } | ||
|
|
||
| return new googleMapsSdk.places.AutocompleteService(); | ||
| }); | ||
| } | ||
| private convertToRegionCodes( | ||
| country: string | string[] | null | undefined, | ||
| ): string[] | undefined { | ||
| if (!country) return undefined; | ||
|
|
||
| return this._autocompletePromise; | ||
| return Array.isArray(country) ? country : [country]; | ||
| } | ||
|
|
||
| getPlacesServices(): Promise<google.maps.places.PlacesService> { | ||
| const node = document.createElement('div'); | ||
| async getSuggestions( | ||
| input: string, | ||
| types: string[], | ||
| componentRestrictions?: google.maps.places.ComponentRestrictions, | ||
| ): Promise<google.maps.places.AutocompleteSuggestion[]> { | ||
| const { AutocompleteSuggestion } = await this._scriptLoader.loadPlacesLibrary(this._apiKey); | ||
| const includedRegionCodes = this.convertToRegionCodes(componentRestrictions?.country); | ||
|
|
||
| const { suggestions } = await AutocompleteSuggestion.fetchAutocompleteSuggestions({ | ||
| input, | ||
| includedPrimaryTypes: types, | ||
| includedRegionCodes, | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
| }); | ||
|
|
||
| return suggestions; | ||
| } | ||
|
|
||
| if (!this._placesPromise) { | ||
| this._placesPromise = this._scriptLoader | ||
| .loadMapsSdk(this._apiKey) | ||
| .then((googleMapsSdk) => { | ||
| if (!googleMapsSdk.places.PlacesService) { | ||
| throw new Error('`PlacesService` is undefined'); | ||
| } | ||
| async getPlaceDetails(placeId: string, fields: string[]): Promise<google.maps.places.Place> { | ||
| const { Place } = await this._scriptLoader.loadPlacesLibrary(this._apiKey); | ||
| const place = new Place({ id: placeId }); | ||
|
|
||
| return new googleMapsSdk.places.PlacesService(node); | ||
| }); | ||
| } | ||
| await place.fetchFields({ fields }); | ||
|
|
||
| return this._placesPromise; | ||
| return place; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.