import { User } from 'features/user/userSlice'; import delay from 'utils/delay'; import { defaultPreferences } from './configuration'; const { REACT_APP_GAPI_KEY: apiKey, REACT_APP_GAPI_CLIENT_ID: clientId, REACT_APP_PROJECT_ID: projectId, REACT_APP_SHEET_ID: spreadsheetId, } = process.env as { [key: string]: string }; const scope = 'https://www.googleapis.com/auth/bigquery.readonly https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/contacts.readonly'; const discoveryDocs = [ 'https://sheets.googleapis.com/$discovery/rest?version=v4', 'https://bigquery.googleapis.com/$discovery/rest?version=v2', 'https://people.googleapis.com/$discovery/rest?version=v1', ]; const allowedDomains = [ 'edg.com.au', 'bws.com.au', 'danmurphys.com.au', 'stores.woolworths.com.au', 'woolworths.com.au', ]; type UserPreferences = Pick; interface MetadataPart { primary: boolean; } interface ProfilePart { metadata: MetadataPart; value: string; displayName: string; url: string; } interface GetQueryProps { query: string; withHeaders?: boolean; onProgressChange?: (progress: number) => void; } class Google { isActive: boolean = false; constructor() { gapi.load('client:auth2', () => gapi.client.init({ apiKey, clientId, scope, discoveryDocs }).then(() => { this.isActive = true; }), ); } attachListener = (listener: (signedIn: boolean) => void) => { if (!this.isActive) throw new Error('api has not been activated'); gapi.auth2.getAuthInstance().isSignedIn.listen(listener); listener(gapi.auth2.getAuthInstance().isSignedIn.get()); }; signOut = () => { gapi.auth2.getAuthInstance().signOut(); }; signIn = () => { gapi.auth2.getAuthInstance().signIn(); }; async getUserData(): Promise> { const user = gapi.auth2.getAuthInstance().currentUser.get(); const userProfile = user.getBasicProfile(); const token = user.getAuthResponse().access_token; const name = userProfile.getName(); const email = userProfile.getEmail(); const image = userProfile.getImageUrl(); const id = userProfile.getId(); const ref = await this.getUserRef(id); const preferences = await this.getUserPreferences(ref); return { ref, preferences, token, id, profile: { name, email, image, }, }; } get active() { return new Promise((resolve, reject) => { if (this.isActive) resolve(this.isActive); let count = 0; const interval = setInterval(() => { if (this.isActive) { resolve(this.isActive); clearInterval(interval); } count += 1; if (count > 200) reject(new Error('Connection Timeout')); }, 100); }); } get validDomain() { return allowedDomains.includes( gapi.auth2 .getAuthInstance() .currentUser.get() .getHostedDomain(), ); } getUserRef(): Promise[]>; getUserRef(id: User['id']): Promise; async getUserRef( id?: User['id'], ): Promise[]> { const { result } = await gapi.client.sheets.spreadsheets.values.get({ spreadsheetId, range: 'Users!A:A', }); if (!result?.values) return id ? this.createUser(id) : []; return id ? result.values.flat().indexOf(id) + 1 || this.createUser(id) : result.values.map(([id], i) => ({ id, ref: i + 1 })); } async createUser(id: User['id']): Promise { const { result } = await gapi.client.sheets.spreadsheets.values.append({ spreadsheetId, range: 'Users!A:B', valueInputOption: 'RAW', insertDataOption: 'INSERT_ROWS', includeValuesInResponse: true, resource: { values: [[id, JSON.stringify(defaultPreferences)]] }, }); if (!result?.updates?.updatedRange) throw new Error('User could not be created'); const reg = /^Users!A(\d+)/g; const [, ref] = reg.exec(result.updates.updatedRange) || []; const finalRef = parseInt(ref, 10); if (!isFinite(finalRef)) throw new Error('User could not be created'); return finalRef; } async getUserPreferences(ref: User['ref']): Promise { const { result } = await gapi.client.sheets.spreadsheets.values.get({ spreadsheetId, range: `Users!B${ref}`, }); if (!result?.values?.[0]?.[0]) return defaultPreferences; return JSON.parse(result.values[0][0]); } setUserPreferences({ ref, preferences, id, }: Partial>) { const row = []; if (id) row.push(id); row.push(JSON.stringify(preferences)); return gapi.client.sheets.spreadsheets.values .update({ spreadsheetId, range: `Users!${id ? 'A' : 'B'}${ref}:B${ref}`, resource: { values: [row] }, valueInputOption: 'RAW', }) .then(() => ({ id, preferences, ref })); } async getUserDetails(userId: string) { const { result } = await gapi.client.people.people.get({ resourceName: `people/${userId}`, personFields: 'names,photos,emailAddresses', }); const { emailAddresses, names, photos } = result; const findPrimary = (arr: ProfilePart[]) => arr.find(({ metadata }) => metadata.primary); const email = findPrimary(emailAddresses)?.value || 'Unknown'; const name = findPrimary(names)?.displayName || 'Unknown'; const image = findPrimary(photos)?.url || 'Unknown'; return { email, name, image }; } async getUsers(): Promise[]> { const users = await this.getUserRef(); return Promise.all( users.map(async ({ id, ref }) => { const profile = await this.getUserDetails(id); const preferences = await this.getUserPreferences(ref); return { id, profile, preferences, ref }; }), ); } async getQuery({ query, withHeaders, onProgressChange }: GetQueryProps) { if (onProgressChange) onProgressChange(0); let { result } = await gapi.client.bigquery.jobs.query({ projectId, query, useLegacySql: false, }); const jobId = result.jobReference?.jobId; if (!jobId) return 'Query failed'; const getResults = () => gapi.client.bigquery.jobs.getQueryResults({ jobId, projectId, }); while (!result.jobComplete) { const response = await delay< ReturnType >(getResults); result = response.result; } const totalRows = result.totalRows; if (!result.rows || !totalRows || !result.schema) return 'No results'; let rows = []; if (withHeaders) rows.push(result.schema.fields?.map(({ name }) => name)); rows = rows.concat(result.rows.map(({ f }) => f?.map(({ v }) => v))); const progress = () => Math.round((rows.length / parseInt(totalRows, 10)) * 10000) / 10000; if (onProgressChange) onProgressChange(progress()); while (result.pageToken) { const response = await gapi.client.bigquery.jobs.getQueryResults({ jobId, projectId, pageToken: result.pageToken, }); result = response.result; rows = rows.concat(result.rows?.map(({ f }) => f?.map(({ v }) => v))); if (onProgressChange) onProgressChange(progress()); } return rows; } } const google = new Google(); export default google;