From b48c4e0bc901e6bc8bc0abb28c9d5dd66d52a78f Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Wed, 9 Jan 2019 16:01:43 +0800 Subject: [PATCH 01/26] Implement ILoginProvider, BypassStore. WIP. --- src/components/Router.tsx | 4 +- src/components/TestRegister.tsx | 8 +++- src/store/BypassStore.ts | 42 +++++++++++++++++++ src/store/PersistableModel.ts | 1 + src/store/index.ts | 2 + third-party/wocky-client/src/index.ts | 1 + .../wocky-client/src/store/ILoginProvider.ts | 9 ++++ third-party/wocky-client/src/store/Wocky.ts | 33 ++++++++------- .../wocky-client/src/transport/utils.ts | 30 +------------ 9 files changed, 81 insertions(+), 49 deletions(-) create mode 100644 src/store/BypassStore.ts create mode 100644 third-party/wocky-client/src/store/ILoginProvider.ts diff --git a/src/components/Router.tsx b/src/components/Router.tsx index 214a722d4..a2193b356 100644 --- a/src/components/Router.tsx +++ b/src/components/Router.tsx @@ -213,9 +213,9 @@ class TinyRobotRouter extends React.Component { } // TODO: Move it outside - login = async (data = {}) => { + login = async (data) => { try { - await this.props.wocky!.login(data) // Remove that after new typings for MST3 + await this.props.wocky!.login(data.providerName) // Remove that after new typings for MST3 return true } catch (error) { this.props.analytics.track('error_connection', {error}) diff --git a/src/components/TestRegister.tsx b/src/components/TestRegister.tsx index f5fe0f9c0..836d1f73a 100644 --- a/src/components/TestRegister.tsx +++ b/src/components/TestRegister.tsx @@ -5,6 +5,7 @@ import {Actions} from 'react-native-router-flux' import {k, width} from './Global' import {colors} from '../constants' import {INavStore} from '../store/NavStore' +import {IBypassStore} from '../store/BypassStore' import {IWocky} from 'wocky-client' type Props = { @@ -13,13 +14,14 @@ type Props = { navStore?: INavStore name: string warn?: any + bypassStore?: IBypassStore } type State = { text: string } -@inject('wocky', 'analytics', 'warn', 'navStore') +@inject('wocky', 'analytics', 'warn', 'navStore', 'bypassStore') @observer class TestRegister extends React.Component { state: State = { @@ -30,7 +32,9 @@ class TestRegister extends React.Component { if (this.props.navStore!.scene !== this.props.name) { return } - Actions.connect({phoneNumber: `+1555${this.state.text}`}) + + this.props.bypassStore!.setPhone(`+1555${this.state.text}`) + Actions.connect({providerName: this.props.bypassStore!.providerName}) } render() { diff --git a/src/store/BypassStore.ts b/src/store/BypassStore.ts new file mode 100644 index 000000000..88fc57fea --- /dev/null +++ b/src/store/BypassStore.ts @@ -0,0 +1,42 @@ +import {types} from 'mobx-state-tree' + +// This class implements ILoginProvider from 'wocky-client' +// but I don't think there's a way to declare this with MST stores? +const BypassStore = types + .model('BypassStore', { + phone: '', + }) + .views(self => { + return { + get providerName() { + // This needs to match the member variable of getRoot/getParent that points to an instance of this object + // Is there a way to auto-discover this? + return 'bypassStore' + }, + } + }) + .actions(self => { + function setPhone(phone) { + self.phone = phone + } + + function getLoginCredentials() { + // Since users need to have unique `sub`s so we'll just use phoneNumber in the case of a bypass login + // https://hippware.slack.com/archives/C033TRJDD/p1543459452073900 + return self.phone ? {typ: 'bypass', sub: self.phone, phone_number: self.phone} : {} + } + + function providerLogout() { + self.phone = '' + } + + return { + setPhone, + getLoginCredentials, + providerLogout, + } + }) + +export default BypassStore +type BypassStoreType = typeof BypassStore.Type +export interface IBypassStore extends BypassStoreType {} diff --git a/src/store/PersistableModel.ts b/src/store/PersistableModel.ts index 35bc8e024..2dc39b56e 100644 --- a/src/store/PersistableModel.ts +++ b/src/store/PersistableModel.ts @@ -5,6 +5,7 @@ import {settings} from '../globals' export const cleanState = { firebaseStore: {}, + bypassStore: {}, locationStore: {}, searchStore: {}, profileValidationStore: {}, diff --git a/src/store/index.ts b/src/store/index.ts index e87145342..8515d5574 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -11,6 +11,7 @@ import * as logger from '../utils/log' import analytics, {Analytics} from '../utils/analytics' import PersistableModel from './PersistableModel' import FirebaseStore from './FirebaseStore' +import BypassStore from './BypassStore' import fileService from './fileService' import LocationStore from './LocationStore' import SearchStore from './SearchStore' @@ -79,6 +80,7 @@ const Store = types wocky: Wocky, homeStore: HomeStore, firebaseStore: FirebaseStore, + bypassStore: BypassStore, locationStore: LocationStore, searchStore: SearchStore, profileValidationStore: ProfileValidationStore, diff --git a/third-party/wocky-client/src/index.ts b/third-party/wocky-client/src/index.ts index 21725d511..b9b40b1d6 100644 --- a/third-party/wocky-client/src/index.ts +++ b/third-party/wocky-client/src/index.ts @@ -1,4 +1,5 @@ export {Wocky, IWocky} from './store/Wocky' +export {ILoginProvider} from './store/ILoginProvider' export {Profile, IProfile} from './model/Profile' export {Base, SERVICE_NAME} from './model/Base' export {Chat, IChat} from './model/Chat' diff --git a/third-party/wocky-client/src/store/ILoginProvider.ts b/third-party/wocky-client/src/store/ILoginProvider.ts new file mode 100644 index 000000000..0919737a0 --- /dev/null +++ b/third-party/wocky-client/src/store/ILoginProvider.ts @@ -0,0 +1,9 @@ +export interface ILoginProvider { + providerName: string + + // Returns an object of JWT fields + getLoginCredentials(): {} + + // Notifies the provider of logout + providerLogout() +} diff --git a/third-party/wocky-client/src/store/Wocky.ts b/third-party/wocky-client/src/store/Wocky.ts index 2a3e2bf02..7bd563728 100644 --- a/third-party/wocky-client/src/store/Wocky.ts +++ b/third-party/wocky-client/src/store/Wocky.ts @@ -1,4 +1,4 @@ -import {types, getParent, getEnv, flow, Instance} from 'mobx-state-tree' +import {types, getParent, getEnv, flow, Instance, getRoot} from 'mobx-state-tree' import {reaction, IReactionDisposer} from 'mobx' import {OwnProfile} from '../model/OwnProfile' import {Profile, IProfile, IProfilePartial} from '../model/Profile' @@ -20,7 +20,6 @@ import {PaginableLoadType, PaginableLoadPromise, Transport} from '../transport/T import {MediaUploadParams} from '../transport/types' import {ILocation, ILocationSnapshot} from '../model/Location' -export type LoginParams = {phoneNumber?: string; accessToken?: string} export const Wocky = types .compose( Base, @@ -29,6 +28,7 @@ export const Wocky = types id: 'wocky', username: types.maybeNull(types.string), password: types.maybeNull(types.string), + providerName: types.maybeNull(types.string), accessToken: types.maybe(types.string), phoneNumber: types.maybe(types.string), host: types.string, @@ -97,25 +97,26 @@ export const Wocky = types // }, }, actions: { - login: flow(function*({phoneNumber, accessToken}: LoginParams) { - if (phoneNumber) { - self.phoneNumber = phoneNumber - } - if (accessToken) { - self.accessToken = accessToken + login: flow(function*(providerName: string) { + const provider = getRoot(self)[providerName] + if (!provider) return false + + // Allow provider to override any default values + const payload = { + aud: 'Wocky', + jti: /*self.username = */ uuid(), + iss: appInfo.uaString, + dvc: appInfo.uniqueId, + ...provider.getLoginCredentials(), } - self.password = generateWockyToken({ - phoneNumber: self.phoneNumber, - accessToken: self.accessToken, - userId: (self.username = uuid()), - uaString: appInfo.uaString, - deviceId: appInfo.uniqueId, - }) + + self.password = generateWockyToken(payload) const res = yield self.transport.login(self.password, self.host) if (!res) { return false } - // set username + + self.providerName = providerName if (self.transport.username) { self.username = self.transport.username } diff --git a/third-party/wocky-client/src/transport/utils.ts b/third-party/wocky-client/src/transport/utils.ts index 7fdf86284..84c8ecee4 100644 --- a/third-party/wocky-client/src/transport/utils.ts +++ b/third-party/wocky-client/src/transport/utils.ts @@ -438,35 +438,7 @@ export function convertNotifications(notifications: any[]): IEventData[] { return notifications.map(convertNotification).filter(x => x) as IEventData[] } -type TokenParams = { - userId: string - uaString: string - deviceId: string - bypass?: boolean - accessToken?: string - phoneNumber?: string -} - -export function generateWockyToken({ - userId, - accessToken, - uaString, - deviceId, - phoneNumber, -}: TokenParams): string { - assert(!!phoneNumber || accessToken !== undefined, `Access token required for non-bypass auth.`) - const payload = { - jti: userId, - iss: uaString, - typ: !!phoneNumber ? 'bypass' : 'firebase', - // if there's no accessToken then we're doing a bypass login. - // Since users need to have unique `sub`s so we'll just use phoneNumber in the case of a bypass login - // https://hippware.slack.com/archives/C033TRJDD/p1543459452073900 - sub: accessToken || phoneNumber, - aud: 'Wocky', - phone_number: phoneNumber, - dvc: deviceId, - } +export function generateWockyToken(payload): string { // TODO: store this with react-native-native-env const magicKey = '0xszZmLxKWdYjvjXOxchnV+ttjVYkU1ieymigubkJZ9dqjnl7WPYLYqLhvC10TaH' const header = {alg: 'HS512', typ: 'JWT'} From a961b6f106da00c8dde98235e1dabb73bd05b9e7 Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Thu, 10 Jan 2019 10:59:58 +0800 Subject: [PATCH 02/26] Use ILoginProvider for static typing. --- third-party/wocky-client/src/store/Wocky.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/third-party/wocky-client/src/store/Wocky.ts b/third-party/wocky-client/src/store/Wocky.ts index 7bd563728..aa6050874 100644 --- a/third-party/wocky-client/src/store/Wocky.ts +++ b/third-party/wocky-client/src/store/Wocky.ts @@ -19,6 +19,7 @@ import {IEventData} from '../model/Event' import {PaginableLoadType, PaginableLoadPromise, Transport} from '../transport/Transport' import {MediaUploadParams} from '../transport/types' import {ILocation, ILocationSnapshot} from '../model/Location' +import {ILoginProvider} from './ILoginProvider' export const Wocky = types .compose( @@ -98,7 +99,7 @@ export const Wocky = types }, actions: { login: flow(function*(providerName: string) { - const provider = getRoot(self)[providerName] + const provider = getRoot(self)[providerName] as ILoginProvider if (!provider) return false // Allow provider to override any default values From 61d789023a311789ca344e54d9bbbf7dcf464f6d Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Thu, 10 Jan 2019 11:27:07 +0800 Subject: [PATCH 03/26] Invert logout flow. Rename some logout()s to onLogout(). --- src/store/BypassStore.ts | 10 ++++------ src/store/HomeStore.ts | 2 +- src/store/LocationStore.ts | 4 ++-- src/store/index.ts | 6 +++--- third-party/wocky-client/src/store/ILoginProvider.ts | 2 +- third-party/wocky-client/src/store/Wocky.ts | 9 +++++++++ 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/store/BypassStore.ts b/src/store/BypassStore.ts index 88fc57fea..a650ee8cc 100644 --- a/src/store/BypassStore.ts +++ b/src/store/BypassStore.ts @@ -1,4 +1,4 @@ -import {types} from 'mobx-state-tree' +import {types, flow} from 'mobx-state-tree' // This class implements ILoginProvider from 'wocky-client' // but I don't think there's a way to declare this with MST stores? @@ -26,14 +26,12 @@ const BypassStore = types return self.phone ? {typ: 'bypass', sub: self.phone, phone_number: self.phone} : {} } - function providerLogout() { - self.phone = '' - } - return { setPhone, getLoginCredentials, - providerLogout, + onLogout: flow(function*() { + self.phone = '' + }), } }) diff --git a/src/store/HomeStore.ts b/src/store/HomeStore.ts index 45652cac6..440a141b7 100644 --- a/src/store/HomeStore.ts +++ b/src/store/HomeStore.ts @@ -131,7 +131,7 @@ const HomeStore = types })) .actions(self => { return { - logout() { + onLogout() { applySnapshot(self, {}) }, addBotsToList(bots: IBot[]): void { diff --git a/src/store/LocationStore.ts b/src/store/LocationStore.ts index 3e40865a7..fac9e15ed 100644 --- a/src/store/LocationStore.ts +++ b/src/store/LocationStore.ts @@ -371,7 +371,7 @@ const LocationStore = types BackgroundGeolocation.un('providerchange', self.onProviderChange) } - const logout = flow(function*() { + const onLogout = flow(function*() { yield self.invalidateCredentials() yield BackgroundGeolocation.stop() logger.log(prefix, 'Stop') @@ -382,7 +382,7 @@ const LocationStore = types finish, didMount, willUnmount, - logout, + onLogout, } }) diff --git a/src/store/index.ts b/src/store/index.ts index 8515d5574..ccd9d1714 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -95,9 +95,9 @@ const Store = types })) .actions(self => ({ logout: flow(function*() { - self.homeStore.logout() - self.locationStore.logout() - return yield self.firebaseStore.logout() + self.homeStore.onLogout() + self.locationStore.onLogout() + return yield self.wocky.logout() }), afterCreate() { analytics.identify(self.wocky) diff --git a/third-party/wocky-client/src/store/ILoginProvider.ts b/third-party/wocky-client/src/store/ILoginProvider.ts index 0919737a0..624431355 100644 --- a/third-party/wocky-client/src/store/ILoginProvider.ts +++ b/third-party/wocky-client/src/store/ILoginProvider.ts @@ -5,5 +5,5 @@ export interface ILoginProvider { getLoginCredentials(): {} // Notifies the provider of logout - providerLogout() + onLogout() } diff --git a/third-party/wocky-client/src/store/Wocky.ts b/third-party/wocky-client/src/store/Wocky.ts index aa6050874..8f6e67959 100644 --- a/third-party/wocky-client/src/store/Wocky.ts +++ b/third-party/wocky-client/src/store/Wocky.ts @@ -673,6 +673,15 @@ export const Wocky = types yield self.disablePush() yield self.disconnect() } + + if (self.providerName) { + const provider = getRoot(self)[self.providerName] as ILoginProvider + if (provider) { + yield provider.onLogout() + } + self.providerName = '' + } + self.profile = null clearCache() self.sessionCount = 0 From f581e711dbc379b8b05d3c7f9afde8ed62adc161 Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Thu, 10 Jan 2019 11:45:39 +0800 Subject: [PATCH 04/26] Split wocky.logout() into two so errorReporting can call just the 'restart' part. --- src/utils/errorReporting.js | 2 +- third-party/wocky-client/src/store/Wocky.ts | 31 ++++++++++++--------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/utils/errorReporting.js b/src/utils/errorReporting.js index b23caf9ba..71a1b52d3 100644 --- a/src/utils/errorReporting.js +++ b/src/utils/errorReporting.js @@ -23,7 +23,7 @@ export default function bugsnag(wocky: any) { } bsClient.notify(error) // TODO: figure out a more elegant way of "restarting" from scratch - await wocky.logout() + await wocky.restart() ErrorUtils.setGlobalHandler(originalGlobalErrorHandler) setTimeout(() => { throw error diff --git a/third-party/wocky-client/src/store/Wocky.ts b/third-party/wocky-client/src/store/Wocky.ts index 8f6e67959..99187f60a 100644 --- a/third-party/wocky-client/src/store/Wocky.ts +++ b/third-party/wocky-client/src/store/Wocky.ts @@ -666,14 +666,26 @@ export const Wocky = types reaction(() => self.transport.botVisitor, self._onBotVisitor), ] } + + const restart = flow(function*() { + if (self.connected) { + yield self.disablePush() + yield self.disconnect() + } + + self.profile = null + clearCache() + self.sessionCount = 0 + self.username = null + self.password = null + self.phoneNumber = undefined + self.accessToken = undefined + }) + return { clearCache, + restart, logout: flow(function* logout() { - if (self.connected) { - yield self.disablePush() - yield self.disconnect() - } - if (self.providerName) { const provider = getRoot(self)[self.providerName] as ILoginProvider if (provider) { @@ -681,14 +693,7 @@ export const Wocky = types } self.providerName = '' } - - self.profile = null - clearCache() - self.sessionCount = 0 - self.username = null - self.password = null - self.phoneNumber = undefined - self.accessToken = undefined + restart() }), afterCreate: () => { self.notifications.setRequest(self._loadNotifications) From b02777b34d17c19a2f750b58f2d260e5f8cfb0b0 Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Thu, 10 Jan 2019 11:58:14 +0800 Subject: [PATCH 05/26] Move Router.login() into store. --- src/components/Router.tsx | 13 +------------ src/store/index.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/Router.tsx b/src/components/Router.tsx index a2193b356..b2c87101b 100644 --- a/src/components/Router.tsx +++ b/src/components/Router.tsx @@ -106,7 +106,7 @@ class TinyRobotRouter extends React.Component { wocky!.username && wocky!.password && wocky!.host} success="checkProfile" failure="preConnection" /> - + wocky!.profile} success="checkHandle" failure="connect" /> wocky!.profile!.handle} success="checkOnboarded" failure="signUp" /> onceStore!.onboarded} success="logged" failure="onboarding" /> @@ -211,17 +211,6 @@ class TinyRobotRouter extends React.Component { this.props.store!.searchStore.setGlobal('') Actions.pop() } - - // TODO: Move it outside - login = async (data) => { - try { - await this.props.wocky!.login(data.providerName) // Remove that after new typings for MST3 - return true - } catch (error) { - this.props.analytics.track('error_connection', {error}) - } - return false - } } export default TinyRobotRouter diff --git a/src/store/index.ts b/src/store/index.ts index ccd9d1714..fea93e7ec 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -94,6 +94,15 @@ const Store = types }, })) .actions(self => ({ + login: flow(function*(data) { + try { + yield self.wocky.login(data.providerName) + return true + } catch (error) { + analytics.track('error_connection', {error}) + } + return false + }), logout: flow(function*() { self.homeStore.onLogout() self.locationStore.onLogout() From 382e0ca5805f8eb293ee7884737bf134d017b8eb Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Thu, 10 Jan 2019 13:40:32 +0800 Subject: [PATCH 06/26] Make firebaseStore a iLoginProvider. Other tidy-up. --- src/components/Connectivity.tsx | 2 +- src/store/FirebaseStore.ts | 31 +++++++++++++------ third-party/wocky-client/src/store/Wocky.ts | 2 -- .../wocky-client/src/transport/types.ts | 12 ------- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/components/Connectivity.tsx b/src/components/Connectivity.tsx index 6b7c84faa..977d981c9 100644 --- a/src/components/Connectivity.tsx +++ b/src/components/Connectivity.tsx @@ -73,7 +73,7 @@ export default class Connectivity extends React.Component { AppState.currentState === 'active' && !model.connected && !model.connecting && - (model.phoneNumber || model.accessToken) && + model.providerName && model.username && model.password && model.host diff --git a/src/store/FirebaseStore.ts b/src/store/FirebaseStore.ts index 1a8acbe7c..f1d35f8b4 100644 --- a/src/store/FirebaseStore.ts +++ b/src/store/FirebaseStore.ts @@ -27,6 +27,15 @@ const FirebaseStore = types registered: false, errorMessage: '', // to avoid strange typescript errors when set it to string or null, })) + .views(self => { + return { + get providerName() { + // This needs to match the member variable of getRoot/getParent that points to an instance of this object + // Is there a way to auto-discover this? + return 'firebaseStore' + }, + } + }) .actions(self => ({ setState(state: State) { Object.assign(self, state) @@ -110,7 +119,15 @@ const FirebaseStore = types } } - const logout = flow(function*() { + const getLoginCredentials = function() { + if (self.token) { + return {typ: 'firebase', sub: self.token} + } else { + return {} + } + } + + const onLogout = flow(function*() { analytics.track('logout') if (self.token) { self.token = null @@ -121,12 +138,7 @@ const FirebaseStore = types logger.warn('firebase logout error', err) } } - try { - yield wocky.logout() - } catch (err) { - analytics.track('error_wocky_logout', {error: err}) - logger.warn('wocky logout error', err) - } + self.reset() confirmResult = null return true @@ -197,7 +209,7 @@ const FirebaseStore = types const registerWithToken = flow(function*() { try { self.setState({buttonText: 'Connecting...'}) - yield wocky.login({accessToken: self.token!}) + yield wocky.login(self.providerName) self.setState({buttonText: 'Verify', registered: true}) } catch (err) { logger.warn('RegisterWithToken error', err) @@ -249,7 +261,8 @@ const FirebaseStore = types return { afterAttach, - logout, + getLoginCredentials, + onLogout, beforeDestroy, verifyPhone, confirmCode, diff --git a/third-party/wocky-client/src/store/Wocky.ts b/third-party/wocky-client/src/store/Wocky.ts index 99187f60a..ca98bb7ea 100644 --- a/third-party/wocky-client/src/store/Wocky.ts +++ b/third-party/wocky-client/src/store/Wocky.ts @@ -30,7 +30,6 @@ export const Wocky = types username: types.maybeNull(types.string), password: types.maybeNull(types.string), providerName: types.maybeNull(types.string), - accessToken: types.maybe(types.string), phoneNumber: types.maybe(types.string), host: types.string, sessionCount: 0, @@ -679,7 +678,6 @@ export const Wocky = types self.username = null self.password = null self.phoneNumber = undefined - self.accessToken = undefined }) return { diff --git a/third-party/wocky-client/src/transport/types.ts b/third-party/wocky-client/src/transport/types.ts index edaf4b39b..e31187d1e 100644 --- a/third-party/wocky-client/src/transport/types.ts +++ b/third-party/wocky-client/src/transport/types.ts @@ -6,18 +6,6 @@ export interface IPagingList { count: number } -export type LoginParams = { - userId?: string - token?: string - password?: string - accessToken?: string - host?: string - version?: string - os?: string - deviceName?: string - phoneNumber?: string -} - export type MediaUploadParams = { access?: string file: { From c9758544b41ebc66b1ffb800e5aa11b0ede83dbb Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Thu, 10 Jan 2019 14:01:43 +0800 Subject: [PATCH 07/26] Make getLoginCredentials a flow(function*()) --- src/store/BypassStore.ts | 20 ++++++++------------ src/store/FirebaseStore.ts | 4 ++-- third-party/wocky-client/src/store/Wocky.ts | 2 +- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/store/BypassStore.ts b/src/store/BypassStore.ts index a650ee8cc..d4c4c6fc1 100644 --- a/src/store/BypassStore.ts +++ b/src/store/BypassStore.ts @@ -16,19 +16,15 @@ const BypassStore = types } }) .actions(self => { - function setPhone(phone) { - self.phone = phone - } - - function getLoginCredentials() { - // Since users need to have unique `sub`s so we'll just use phoneNumber in the case of a bypass login - // https://hippware.slack.com/archives/C033TRJDD/p1543459452073900 - return self.phone ? {typ: 'bypass', sub: self.phone, phone_number: self.phone} : {} - } - return { - setPhone, - getLoginCredentials, + setPhone: phone => { + self.phone = phone + }, + getLoginCredentials: flow(function*() { + // Since users need to have unique `sub`s so we'll just use phoneNumber in the case of a bypass login + // https://hippware.slack.com/archives/C033TRJDD/p1543459452073900 + return self.phone ? {typ: 'bypass', sub: self.phone, phone_number: self.phone} : {} + }), onLogout: flow(function*() { self.phone = '' }), diff --git a/src/store/FirebaseStore.ts b/src/store/FirebaseStore.ts index f1d35f8b4..a38f45762 100644 --- a/src/store/FirebaseStore.ts +++ b/src/store/FirebaseStore.ts @@ -119,13 +119,13 @@ const FirebaseStore = types } } - const getLoginCredentials = function() { + const getLoginCredentials = flow(function*() { if (self.token) { return {typ: 'firebase', sub: self.token} } else { return {} } - } + }) const onLogout = flow(function*() { analytics.track('logout') diff --git a/third-party/wocky-client/src/store/Wocky.ts b/third-party/wocky-client/src/store/Wocky.ts index ca98bb7ea..6f88597d8 100644 --- a/third-party/wocky-client/src/store/Wocky.ts +++ b/third-party/wocky-client/src/store/Wocky.ts @@ -107,7 +107,7 @@ export const Wocky = types jti: /*self.username = */ uuid(), iss: appInfo.uaString, dvc: appInfo.uniqueId, - ...provider.getLoginCredentials(), + ...(yield provider.getLoginCredentials()), } self.password = generateWockyToken(payload) From 581f87fdbc5fa1de8f5082954f311207843555ad Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Thu, 10 Jan 2019 14:51:01 +0800 Subject: [PATCH 08/26] Fix up background re-auth. Refresh firebase token. --- src/components/Connectivity.tsx | 2 +- src/store/FirebaseStore.ts | 10 ++++++++++ third-party/wocky-client/src/store/Wocky.ts | 14 +++++++++++--- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/components/Connectivity.tsx b/src/components/Connectivity.tsx index 977d981c9..f958885af 100644 --- a/src/components/Connectivity.tsx +++ b/src/components/Connectivity.tsx @@ -84,7 +84,7 @@ export default class Connectivity extends React.Component { delay: this.retryDelay, connectionInfo: this.connectionInfo, }) - await model.login({}) + await model.login() this.props.analytics.track('reconnect_success', {...info}) this.retryDelay = 1000 } catch (e) { diff --git a/src/store/FirebaseStore.ts b/src/store/FirebaseStore.ts index a38f45762..1977d8750 100644 --- a/src/store/FirebaseStore.ts +++ b/src/store/FirebaseStore.ts @@ -121,6 +121,16 @@ const FirebaseStore = types const getLoginCredentials = flow(function*() { if (self.token) { + // Refresh firebase token if less than 5 minutes from expiry + const tokenResult = yield auth.currentUser!.getIdTokenResult(false) + if ( + tokenResult && + tokenResult.claims.exp && + tokenResult.claims.exp - Date.now() / 1000 < 300 + ) { + self.token = yield auth.currentUser!.getIdToken(true) + } + return {typ: 'firebase', sub: self.token} } else { return {} diff --git a/third-party/wocky-client/src/store/Wocky.ts b/third-party/wocky-client/src/store/Wocky.ts index 6f88597d8..b1ae3b2ca 100644 --- a/third-party/wocky-client/src/store/Wocky.ts +++ b/third-party/wocky-client/src/store/Wocky.ts @@ -97,8 +97,16 @@ export const Wocky = types // }, }, actions: { - login: flow(function*(providerName: string) { - const provider = getRoot(self)[providerName] as ILoginProvider + login: flow(function*(providerName = '') { + let provider = null as ILoginProvider | null + if (providerName) { + provider = getRoot(self)[providerName] as ILoginProvider + } else if (self.providerName) { + // If providerName is not supplied, then reuse existing + // provider if there is one + provider = getRoot(self)[self.providerName] as ILoginProvider + } + if (!provider) return false // Allow provider to override any default values @@ -116,7 +124,7 @@ export const Wocky = types return false } - self.providerName = providerName + self.providerName = provider.providerName if (self.transport.username) { self.username = self.transport.username } From 7079e0f9813766967560e9b3f3e74ce93a4118a9 Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Thu, 10 Jan 2019 15:40:02 +0800 Subject: [PATCH 09/26] Hand-edit mockStore.ts to make test pass. --- __tests__/utils/mockStore.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/__tests__/utils/mockStore.ts b/__tests__/utils/mockStore.ts index 23db1e129..746e3da1c 100644 --- a/__tests__/utils/mockStore.ts +++ b/__tests__/utils/mockStore.ts @@ -62,11 +62,13 @@ export default { buttonText: 'Verify', registered: false, errorMessage: '', + providerName: 'firebaseStore', setState: jest.fn(), reset: jest.fn(), setInviteCode: jest.fn(), afterAttach: jest.fn(), - logout: jest.fn(), + getLoginCredentials: jest.fn(), + onLogout: jest.fn(), beforeDestroy: jest.fn(), verifyPhone: jest.fn(), confirmCode: jest.fn(), From 4e1a9dba00d9c60245f17d2490511d1ad6a9f962 Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Mon, 14 Jan 2019 15:53:36 +0800 Subject: [PATCH 10/26] Implement simple provider registry. More stable names. --- src/store/BypassStore.ts | 14 +++++------- src/store/FirebaseStore.ts | 13 +++-------- third-party/wocky-client/src/index.ts | 2 +- .../wocky-client/src/store/ILoginProvider.ts | 9 -------- .../wocky-client/src/store/LoginProvider.ts | 22 +++++++++++++++++++ third-party/wocky-client/src/store/Wocky.ts | 10 ++++----- 6 files changed, 36 insertions(+), 34 deletions(-) delete mode 100644 third-party/wocky-client/src/store/ILoginProvider.ts create mode 100644 third-party/wocky-client/src/store/LoginProvider.ts diff --git a/src/store/BypassStore.ts b/src/store/BypassStore.ts index d4c4c6fc1..924e04b8a 100644 --- a/src/store/BypassStore.ts +++ b/src/store/BypassStore.ts @@ -1,22 +1,18 @@ import {types, flow} from 'mobx-state-tree' +import {registerProvider} from 'wocky-client' // This class implements ILoginProvider from 'wocky-client' // but I don't think there's a way to declare this with MST stores? const BypassStore = types .model('BypassStore', { phone: '', - }) - .views(self => { - return { - get providerName() { - // This needs to match the member variable of getRoot/getParent that points to an instance of this object - // Is there a way to auto-discover this? - return 'bypassStore' - }, - } + providerName: 'bypass', }) .actions(self => { return { + afterAttach: () => { + registerProvider(self.providerName, self as any) + }, setPhone: phone => { self.phone = phone }, diff --git a/src/store/FirebaseStore.ts b/src/store/FirebaseStore.ts index 1977d8750..26f718aad 100644 --- a/src/store/FirebaseStore.ts +++ b/src/store/FirebaseStore.ts @@ -1,6 +1,6 @@ import {types, getEnv, flow, getParent} from 'mobx-state-tree' import {when} from 'mobx' -import {IWocky} from 'wocky-client' +import {IWocky, registerProvider} from 'wocky-client' import {IEnv} from '.' import {settings} from '../globals' @@ -18,6 +18,7 @@ const codeUrlString = '?inviteCode=' const FirebaseStore = types .model('FirebaseStore', { phone: '', + providerName: 'firebase', token: types.maybeNull(types.string), resource: types.maybeNull(types.string), inviteCode: types.maybeNull(types.string), @@ -27,15 +28,6 @@ const FirebaseStore = types registered: false, errorMessage: '', // to avoid strange typescript errors when set it to string or null, })) - .views(self => { - return { - get providerName() { - // This needs to match the member variable of getRoot/getParent that points to an instance of this object - // Is there a way to auto-discover this? - return 'firebaseStore' - }, - } - }) .actions(self => ({ setState(state: State) { Object.assign(self, state) @@ -67,6 +59,7 @@ const FirebaseStore = types } function afterAttach() { + registerProvider(self.providerName, self as any) auth.onAuthStateChanged(processFirebaseAuthChange) wocky = (getParent(self) as any).wocky // wocky could be null for HMR (?) // setup dynamic links diff --git a/third-party/wocky-client/src/index.ts b/third-party/wocky-client/src/index.ts index b9b40b1d6..55d1c3bea 100644 --- a/third-party/wocky-client/src/index.ts +++ b/third-party/wocky-client/src/index.ts @@ -1,5 +1,5 @@ export {Wocky, IWocky} from './store/Wocky' -export {ILoginProvider} from './store/ILoginProvider' +export {ILoginProvider, registerProvider, getProvider} from './store/LoginProvider' export {Profile, IProfile} from './model/Profile' export {Base, SERVICE_NAME} from './model/Base' export {Chat, IChat} from './model/Chat' diff --git a/third-party/wocky-client/src/store/ILoginProvider.ts b/third-party/wocky-client/src/store/ILoginProvider.ts deleted file mode 100644 index 624431355..000000000 --- a/third-party/wocky-client/src/store/ILoginProvider.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface ILoginProvider { - providerName: string - - // Returns an object of JWT fields - getLoginCredentials(): {} - - // Notifies the provider of logout - onLogout() -} diff --git a/third-party/wocky-client/src/store/LoginProvider.ts b/third-party/wocky-client/src/store/LoginProvider.ts new file mode 100644 index 000000000..ab8fa550d --- /dev/null +++ b/third-party/wocky-client/src/store/LoginProvider.ts @@ -0,0 +1,22 @@ +export interface ILoginProvider { + providerName: string + + // Returns an object of JWT fields + // This is actually a function iterator. How to declare in an interface? + getLoginCredentials(): {} + + // Notifies the provider of logout + // This is actually a function iterator. How to declare in an interface? + onLogout() +} + +// A simple registry of provider objects +const providers = {} + +export function registerProvider(name: string, provider: ILoginProvider) { + providers[name] = provider +} + +export function getProvider(name: string): ILoginProvider { + return providers[name] +} diff --git a/third-party/wocky-client/src/store/Wocky.ts b/third-party/wocky-client/src/store/Wocky.ts index b1ae3b2ca..5b6b8809b 100644 --- a/third-party/wocky-client/src/store/Wocky.ts +++ b/third-party/wocky-client/src/store/Wocky.ts @@ -1,4 +1,4 @@ -import {types, getParent, getEnv, flow, Instance, getRoot} from 'mobx-state-tree' +import {types, getParent, getEnv, flow, Instance} from 'mobx-state-tree' import {reaction, IReactionDisposer} from 'mobx' import {OwnProfile} from '../model/OwnProfile' import {Profile, IProfile, IProfilePartial} from '../model/Profile' @@ -19,7 +19,7 @@ import {IEventData} from '../model/Event' import {PaginableLoadType, PaginableLoadPromise, Transport} from '../transport/Transport' import {MediaUploadParams} from '../transport/types' import {ILocation, ILocationSnapshot} from '../model/Location' -import {ILoginProvider} from './ILoginProvider' +import {ILoginProvider, getProvider} from './LoginProvider' export const Wocky = types .compose( @@ -100,11 +100,11 @@ export const Wocky = types login: flow(function*(providerName = '') { let provider = null as ILoginProvider | null if (providerName) { - provider = getRoot(self)[providerName] as ILoginProvider + provider = getProvider(providerName) } else if (self.providerName) { // If providerName is not supplied, then reuse existing // provider if there is one - provider = getRoot(self)[self.providerName] as ILoginProvider + provider = getProvider(self.providerName) } if (!provider) return false @@ -693,7 +693,7 @@ export const Wocky = types restart, logout: flow(function* logout() { if (self.providerName) { - const provider = getRoot(self)[self.providerName] as ILoginProvider + const provider = getProvider(self.providerName) if (provider) { yield provider.onLogout() } From f6a2bfb04a1eb110c5bf486838aafe8d949c9424 Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Mon, 14 Jan 2019 16:03:19 +0800 Subject: [PATCH 11/26] Revert f581e711, remove errorReporting.js. --- src/utils/errorReporting.js | 33 --------------------- third-party/wocky-client/src/store/Wocky.ts | 28 ++++++++--------- 2 files changed, 12 insertions(+), 49 deletions(-) delete mode 100644 src/utils/errorReporting.js diff --git a/src/utils/errorReporting.js b/src/utils/errorReporting.js deleted file mode 100644 index 71a1b52d3..000000000 --- a/src/utils/errorReporting.js +++ /dev/null @@ -1,33 +0,0 @@ -/* global ErrorUtils */ - -import NativeEnv from 'react-native-native-env' -import {Client, Configuration} from 'bugsnag-react-native' - -const config = new Configuration() -config.notifyReleaseStages = ['testflight', 'production'] -const bsClient = new Client(config) - -export default function bugsnag(wocky: any) { - if (!NativeEnv.get('DEBUG')) { - // if there is an uncaught js error, logout and rethrow shortly thereafter - // there is technically a race condition to see if the network request can complete in time to crash - const originalGlobalErrorHandler = ErrorUtils.getGlobalHandler() - ErrorUtils.setGlobalHandler(async error => { - try { - const {profile} = wocky - if (profile) { - bsClient.setUser(profile.id, profile.displayName, profile && profile.email) - } - } catch (err) { - // intentionally swallow these errors to prevent crashes before bugsnag sending - } - bsClient.notify(error) - // TODO: figure out a more elegant way of "restarting" from scratch - await wocky.restart() - ErrorUtils.setGlobalHandler(originalGlobalErrorHandler) - setTimeout(() => { - throw error - }, 1000) - }) - } -} diff --git a/third-party/wocky-client/src/store/Wocky.ts b/third-party/wocky-client/src/store/Wocky.ts index 5b6b8809b..45ad7602d 100644 --- a/third-party/wocky-client/src/store/Wocky.ts +++ b/third-party/wocky-client/src/store/Wocky.ts @@ -674,24 +674,14 @@ export const Wocky = types ] } - const restart = flow(function*() { - if (self.connected) { - yield self.disablePush() - yield self.disconnect() - } - - self.profile = null - clearCache() - self.sessionCount = 0 - self.username = null - self.password = null - self.phoneNumber = undefined - }) - return { clearCache, - restart, logout: flow(function* logout() { + if (self.connected) { + yield self.disablePush() + yield self.disconnect() + } + if (self.providerName) { const provider = getProvider(self.providerName) if (provider) { @@ -699,7 +689,13 @@ export const Wocky = types } self.providerName = '' } - restart() + + self.profile = null + clearCache() + self.sessionCount = 0 + self.username = null + self.password = null + self.phoneNumber = undefined }), afterCreate: () => { self.notifications.setRequest(self._loadNotifications) From 61168dd8f10c4186a8e4f1db77d7e9c03b0dc1f7 Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Tue, 15 Jan 2019 13:43:57 +0800 Subject: [PATCH 12/26] Strong typing for generateWockyToken() --- third-party/wocky-client/src/transport/utils.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/third-party/wocky-client/src/transport/utils.ts b/third-party/wocky-client/src/transport/utils.ts index 84c8ecee4..b57edd643 100644 --- a/third-party/wocky-client/src/transport/utils.ts +++ b/third-party/wocky-client/src/transport/utils.ts @@ -438,7 +438,17 @@ export function convertNotifications(notifications: any[]): IEventData[] { return notifications.map(convertNotification).filter(x => x) as IEventData[] } -export function generateWockyToken(payload): string { +type TokenParams = { + jti: string + iss: string + aud?: string + dvc: string + typ: string + sub: string + phone_number?: string +} + +export function generateWockyToken(payload: TokenParams): string { // TODO: store this with react-native-native-env const magicKey = '0xszZmLxKWdYjvjXOxchnV+ttjVYkU1ieymigubkJZ9dqjnl7WPYLYqLhvC10TaH' const header = {alg: 'HS512', typ: 'JWT'} From e7c403076479e7baac4568afcecb50d3b7004b26 Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Tue, 15 Jan 2019 13:57:49 +0800 Subject: [PATCH 13/26] Refine JWT fields. --- third-party/wocky-client/src/store/Wocky.ts | 3 ++- third-party/wocky-client/src/transport/utils.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/third-party/wocky-client/src/store/Wocky.ts b/third-party/wocky-client/src/store/Wocky.ts index 45ad7602d..f3b5a41da 100644 --- a/third-party/wocky-client/src/store/Wocky.ts +++ b/third-party/wocky-client/src/store/Wocky.ts @@ -112,9 +112,10 @@ export const Wocky = types // Allow provider to override any default values const payload = { aud: 'Wocky', - jti: /*self.username = */ uuid(), + jti: uuid(), iss: appInfo.uaString, dvc: appInfo.uniqueId, + iat: Math.floor(Date.now() / 1000), ...(yield provider.getLoginCredentials()), } diff --git a/third-party/wocky-client/src/transport/utils.ts b/third-party/wocky-client/src/transport/utils.ts index b57edd643..110d30678 100644 --- a/third-party/wocky-client/src/transport/utils.ts +++ b/third-party/wocky-client/src/transport/utils.ts @@ -446,6 +446,7 @@ type TokenParams = { typ: string sub: string phone_number?: string + iat?: number } export function generateWockyToken(payload: TokenParams): string { From c9b0c812f3f090457c55f7a87b1e073e85a3b863 Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Tue, 15 Jan 2019 14:16:18 +0800 Subject: [PATCH 14/26] Move BypassStore into wocky-client. --- src/components/TestRegister.tsx | 3 +-- src/store/index.ts | 2 +- third-party/wocky-client/src/index.ts | 1 + {src => third-party/wocky-client/src}/store/BypassStore.ts | 5 ++--- 4 files changed, 5 insertions(+), 6 deletions(-) rename {src => third-party/wocky-client/src}/store/BypassStore.ts (90%) diff --git a/src/components/TestRegister.tsx b/src/components/TestRegister.tsx index 836d1f73a..b0678cab8 100644 --- a/src/components/TestRegister.tsx +++ b/src/components/TestRegister.tsx @@ -5,8 +5,7 @@ import {Actions} from 'react-native-router-flux' import {k, width} from './Global' import {colors} from '../constants' import {INavStore} from '../store/NavStore' -import {IBypassStore} from '../store/BypassStore' -import {IWocky} from 'wocky-client' +import {IWocky, IBypassStore} from 'wocky-client' type Props = { wocky?: IWocky diff --git a/src/store/index.ts b/src/store/index.ts index fea93e7ec..bbe609914 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -11,7 +11,7 @@ import * as logger from '../utils/log' import analytics, {Analytics} from '../utils/analytics' import PersistableModel from './PersistableModel' import FirebaseStore from './FirebaseStore' -import BypassStore from './BypassStore' +import {BypassStore} from 'wocky-client' import fileService from './fileService' import LocationStore from './LocationStore' import SearchStore from './SearchStore' diff --git a/third-party/wocky-client/src/index.ts b/third-party/wocky-client/src/index.ts index 55d1c3bea..32b5ecbed 100644 --- a/third-party/wocky-client/src/index.ts +++ b/third-party/wocky-client/src/index.ts @@ -22,3 +22,4 @@ export {OwnProfile, IOwnProfile} from './model/OwnProfile' export {Transport} from './transport/Transport' export {IPagingList} from './transport/types' export {AppInfo, IAppInfo} from './store/AppInfo' +export {BypassStore, IBypassStore} from './store/BypassStore' diff --git a/src/store/BypassStore.ts b/third-party/wocky-client/src/store/BypassStore.ts similarity index 90% rename from src/store/BypassStore.ts rename to third-party/wocky-client/src/store/BypassStore.ts index 924e04b8a..a7c948db3 100644 --- a/src/store/BypassStore.ts +++ b/third-party/wocky-client/src/store/BypassStore.ts @@ -1,9 +1,9 @@ import {types, flow} from 'mobx-state-tree' -import {registerProvider} from 'wocky-client' +import {registerProvider} from './LoginProvider' // This class implements ILoginProvider from 'wocky-client' // but I don't think there's a way to declare this with MST stores? -const BypassStore = types +export const BypassStore = types .model('BypassStore', { phone: '', providerName: 'bypass', @@ -27,6 +27,5 @@ const BypassStore = types } }) -export default BypassStore type BypassStoreType = typeof BypassStore.Type export interface IBypassStore extends BypassStoreType {} From f7dc298b03dd916c55f2a7e4485a3e9024840d45 Mon Sep 17 00:00:00 2001 From: Beng Tan Date: Tue, 15 Jan 2019 14:37:01 +0800 Subject: [PATCH 15/26] Fix up (bypass) login for unit tests. --- third-party/wocky-client/test/support/testuser.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/third-party/wocky-client/test/support/testuser.ts b/third-party/wocky-client/test/support/testuser.ts index 73f04ee17..bf67ddf39 100644 --- a/third-party/wocky-client/test/support/testuser.ts +++ b/third-party/wocky-client/test/support/testuser.ts @@ -1,4 +1,4 @@ -import {Wocky, IWocky, Transport} from '../../src' +import {Wocky, IWocky, Transport, BypassStore, registerProvider} from '../../src' import {AppInfo} from '../../src/store/AppInfo' import fileService from './fileService' import {simpleActionLogger} from 'mst-middlewares' @@ -68,9 +68,12 @@ export async function createUser(num?: number, phoneNum?: string): Promise Date: Tue, 15 Jan 2019 14:41:48 +0800 Subject: [PATCH 16/26] Reduce registerProvider params. --- src/store/FirebaseStore.ts | 2 +- third-party/wocky-client/src/store/BypassStore.ts | 2 +- third-party/wocky-client/src/store/LoginProvider.ts | 4 ++-- third-party/wocky-client/test/support/testuser.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/store/FirebaseStore.ts b/src/store/FirebaseStore.ts index 26f718aad..3ebeb359c 100644 --- a/src/store/FirebaseStore.ts +++ b/src/store/FirebaseStore.ts @@ -59,7 +59,7 @@ const FirebaseStore = types } function afterAttach() { - registerProvider(self.providerName, self as any) + registerProvider(self as any) auth.onAuthStateChanged(processFirebaseAuthChange) wocky = (getParent(self) as any).wocky // wocky could be null for HMR (?) // setup dynamic links diff --git a/third-party/wocky-client/src/store/BypassStore.ts b/third-party/wocky-client/src/store/BypassStore.ts index a7c948db3..a1400652e 100644 --- a/third-party/wocky-client/src/store/BypassStore.ts +++ b/third-party/wocky-client/src/store/BypassStore.ts @@ -11,7 +11,7 @@ export const BypassStore = types .actions(self => { return { afterAttach: () => { - registerProvider(self.providerName, self as any) + registerProvider(self as any) }, setPhone: phone => { self.phone = phone diff --git a/third-party/wocky-client/src/store/LoginProvider.ts b/third-party/wocky-client/src/store/LoginProvider.ts index ab8fa550d..3896ef8e3 100644 --- a/third-party/wocky-client/src/store/LoginProvider.ts +++ b/third-party/wocky-client/src/store/LoginProvider.ts @@ -13,8 +13,8 @@ export interface ILoginProvider { // A simple registry of provider objects const providers = {} -export function registerProvider(name: string, provider: ILoginProvider) { - providers[name] = provider +export function registerProvider(provider: ILoginProvider) { + providers[provider.providerName] = provider } export function getProvider(name: string): ILoginProvider { diff --git a/third-party/wocky-client/test/support/testuser.ts b/third-party/wocky-client/test/support/testuser.ts index bf67ddf39..bded43cc0 100644 --- a/third-party/wocky-client/test/support/testuser.ts +++ b/third-party/wocky-client/test/support/testuser.ts @@ -72,7 +72,7 @@ export async function createUser(num?: number, phoneNum?: string): Promise Date: Fri, 18 Jan 2019 15:05:37 -0800 Subject: [PATCH 17/26] one-way credential data flow to wocky (no need for LoginProvider) --- third-party/wocky-client/src/index.ts | 3 +- .../wocky-client/src/store/BypassStore.ts | 31 --------------- .../wocky-client/src/store/LoginProvider.ts | 22 ----------- third-party/wocky-client/src/store/Wocky.ts | 39 ++++--------------- .../wocky-client/test/support/testuser.ts | 11 +++--- 5 files changed, 14 insertions(+), 92 deletions(-) delete mode 100644 third-party/wocky-client/src/store/BypassStore.ts delete mode 100644 third-party/wocky-client/src/store/LoginProvider.ts diff --git a/third-party/wocky-client/src/index.ts b/third-party/wocky-client/src/index.ts index a643592dd..59150c38d 100644 --- a/third-party/wocky-client/src/index.ts +++ b/third-party/wocky-client/src/index.ts @@ -1,5 +1,4 @@ -export {Wocky, IWocky} from './store/Wocky' -export {ILoginProvider, registerProvider, getProvider} from './store/LoginProvider' +export {Wocky, IWocky, Credentials} from './store/Wocky' export {Profile, IProfile} from './model/Profile' export {Base, SERVICE_NAME} from './model/Base' export {Chat, IChat} from './model/Chat' diff --git a/third-party/wocky-client/src/store/BypassStore.ts b/third-party/wocky-client/src/store/BypassStore.ts deleted file mode 100644 index a1400652e..000000000 --- a/third-party/wocky-client/src/store/BypassStore.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {types, flow} from 'mobx-state-tree' -import {registerProvider} from './LoginProvider' - -// This class implements ILoginProvider from 'wocky-client' -// but I don't think there's a way to declare this with MST stores? -export const BypassStore = types - .model('BypassStore', { - phone: '', - providerName: 'bypass', - }) - .actions(self => { - return { - afterAttach: () => { - registerProvider(self as any) - }, - setPhone: phone => { - self.phone = phone - }, - getLoginCredentials: flow(function*() { - // Since users need to have unique `sub`s so we'll just use phoneNumber in the case of a bypass login - // https://hippware.slack.com/archives/C033TRJDD/p1543459452073900 - return self.phone ? {typ: 'bypass', sub: self.phone, phone_number: self.phone} : {} - }), - onLogout: flow(function*() { - self.phone = '' - }), - } - }) - -type BypassStoreType = typeof BypassStore.Type -export interface IBypassStore extends BypassStoreType {} diff --git a/third-party/wocky-client/src/store/LoginProvider.ts b/third-party/wocky-client/src/store/LoginProvider.ts deleted file mode 100644 index 3896ef8e3..000000000 --- a/third-party/wocky-client/src/store/LoginProvider.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface ILoginProvider { - providerName: string - - // Returns an object of JWT fields - // This is actually a function iterator. How to declare in an interface? - getLoginCredentials(): {} - - // Notifies the provider of logout - // This is actually a function iterator. How to declare in an interface? - onLogout() -} - -// A simple registry of provider objects -const providers = {} - -export function registerProvider(provider: ILoginProvider) { - providers[provider.providerName] = provider -} - -export function getProvider(name: string): ILoginProvider { - return providers[name] -} diff --git a/third-party/wocky-client/src/store/Wocky.ts b/third-party/wocky-client/src/store/Wocky.ts index b15afbd41..f875f8d5f 100644 --- a/third-party/wocky-client/src/store/Wocky.ts +++ b/third-party/wocky-client/src/store/Wocky.ts @@ -19,7 +19,8 @@ import {IEventData} from '../model/Event' import {PaginableLoadType, PaginableLoadPromise} from '../transport/Transport' import {MediaUploadParams} from '../transport/types' import {ILocation, ILocationSnapshot} from '../model/Location' -import {ILoginProvider, getProvider} from './LoginProvider' + +export type Credentials = {typ: string; sub: string; phone_number?: string} export const Wocky = types .compose( @@ -27,9 +28,8 @@ export const Wocky = types Storages, types.model({ id: 'wocky', + // todo: make this volatile? username: types.maybeNull(types.string), - password: types.maybeNull(types.string), - providerName: types.maybeNull(types.string), phoneNumber: types.maybe(types.string), host: types.string, sessionCount: 0, @@ -74,35 +74,22 @@ export const Wocky = types // }, }, actions: { - login: flow(function*(providerName = '') { - let provider = null as ILoginProvider | null - if (providerName) { - provider = getProvider(providerName) - } else if (self.providerName) { - // If providerName is not supplied, then reuse existing - // provider if there is one - provider = getProvider(self.providerName) - } - - if (!provider) return false - - // Allow provider to override any default values + login: flow(function*(credentials: Credentials) { const payload = { aud: 'Wocky', jti: uuid(), iss: appInfo.uaString, dvc: appInfo.uniqueId, iat: Math.floor(Date.now() / 1000), - ...(yield provider.getLoginCredentials()), + ...credentials, } - self.password = generateWockyToken(payload) - const res = yield self.transport.login(self.password, self.host) + const password = generateWockyToken(payload) + const res = yield self.transport.login(password, self.host) if (!res) { return false } - self.providerName = provider.providerName if (self.transport.username) { self.username = self.transport.username } @@ -110,7 +97,7 @@ export const Wocky = types yield self.loadProfile(self.username!) self.sessionCount++ return true - }), + }) as (credentials: Credentials) => Promise, disconnect: flow(function*() { if (self.profile) { self.profile!.status = 'unavailable' @@ -620,20 +607,10 @@ export const Wocky = types yield self.disconnect() } - if (self.providerName) { - const provider = getProvider(self.providerName) - if (provider) { - yield provider.onLogout() - } - self.providerName = '' - } - self.profile = null clearCache() self.sessionCount = 0 self.username = null - self.password = null - self.phoneNumber = undefined }), afterCreate: () => { self.notifications.setRequest(self._loadNotifications) diff --git a/third-party/wocky-client/test/support/testuser.ts b/third-party/wocky-client/test/support/testuser.ts index bded43cc0..08435be05 100644 --- a/third-party/wocky-client/test/support/testuser.ts +++ b/third-party/wocky-client/test/support/testuser.ts @@ -68,12 +68,11 @@ export async function createUser(num?: number, phoneNum?: string): Promise Date: Fri, 18 Jan 2019 17:16:57 -0800 Subject: [PATCH 18/26] wocky is a part of PersistableModel, remove it here --- src/store/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/store/index.ts b/src/store/index.ts index bbe609914..d73a37acb 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -77,7 +77,6 @@ const env = { const Store = types .model('Store', { - wocky: Wocky, homeStore: HomeStore, firebaseStore: FirebaseStore, bypassStore: BypassStore, From 12e86c4c8d34bb36c8138c23febcbaa129715ec2 Mon Sep 17 00:00:00 2001 From: Eric Kirkham Date: Fri, 18 Jan 2019 17:37:59 -0800 Subject: [PATCH 19/26] WIP: reauth improvements --- __tests__/utils/mockStore.ts | 4 +- src/components/Connectivity.tsx | 13 ++-- src/components/Router.tsx | 12 ++-- src/components/TestRegister.tsx | 11 +-- src/store/AuthStore.ts | 68 +++++++++++++++++++ src/store/FirebaseStore.ts | 57 ++++++++-------- src/store/LocationStore.ts | 4 +- src/store/PersistableModel.ts | 6 +- src/store/index.ts | 30 +++----- third-party/wocky-client/src/index.ts | 1 - third-party/wocky-client/src/store/Wocky.ts | 1 - .../wocky-client/test/support/testuser.ts | 2 +- 12 files changed, 132 insertions(+), 77 deletions(-) create mode 100644 src/store/AuthStore.ts diff --git a/__tests__/utils/mockStore.ts b/__tests__/utils/mockStore.ts index 2971a6501..5bed16bc6 100644 --- a/__tests__/utils/mockStore.ts +++ b/__tests__/utils/mockStore.ts @@ -56,7 +56,7 @@ export default { warn: jest.fn(), firebaseStore: { phone: '1234567890', - token: null, + token: undefined as any, resource: null, inviteCode: null, buttonText: 'Verify', @@ -68,7 +68,7 @@ export default { setInviteCode: jest.fn(), afterAttach: jest.fn(), getLoginCredentials: jest.fn(), - onLogout: jest.fn(), + logout: jest.fn(), beforeDestroy: jest.fn(), verifyPhone: jest.fn(), confirmCode: jest.fn(), diff --git a/src/components/Connectivity.tsx b/src/components/Connectivity.tsx index f958885af..69f4dbda9 100644 --- a/src/components/Connectivity.tsx +++ b/src/components/Connectivity.tsx @@ -4,11 +4,10 @@ import {reaction, observable} from 'mobx' import {inject} from 'mobx-react/native' import {Actions} from 'react-native-router-flux' import * as log from '../utils/log' - -// TODO: need to export declaration file to make this work as expected? import {IWocky} from 'wocky-client' import {IHomeStore} from '../store/HomeStore' import {ILocationStore} from '../store/LocationStore' +import {IAuthStore} from 'src/store/AuthStore' type Props = { wocky?: IWocky @@ -17,9 +16,10 @@ type Props = { locationStore?: ILocationStore log?: any analytics?: any + authStore?: IAuthStore } -@inject('wocky', 'homeStore', 'notificationStore', 'locationStore', 'log', 'analytics') +@inject('wocky', 'homeStore', 'notificationStore', 'locationStore', 'log', 'analytics', 'authStore') export default class Connectivity extends React.Component { @observable lastDisconnected = Date.now() retryDelay = 1000 @@ -69,13 +69,12 @@ export default class Connectivity extends React.Component { tryReconnect = async reason => { const info = {reason, currentState: AppState.currentState} const model = this.props.wocky! + const {authStore} = this.props if ( AppState.currentState === 'active' && !model.connected && !model.connecting && - model.providerName && - model.username && - model.password && + authStore!.canLogin && model.host ) { try { @@ -84,7 +83,7 @@ export default class Connectivity extends React.Component { delay: this.retryDelay, connectionInfo: this.connectionInfo, }) - await model.login() + await authStore!.login() this.props.analytics.track('reconnect_success', {...info}) this.retryDelay = 1000 } catch (e) { diff --git a/src/components/Router.tsx b/src/components/Router.tsx index b2c87101b..22ec7a47d 100644 --- a/src/components/Router.tsx +++ b/src/components/Router.tsx @@ -50,6 +50,7 @@ import { IStore } from 'src/store' import { IPersistable } from 'src/store/PersistableModel' import OnboardingSwiper from './Onboarding/OnboardingSwiper' import ChatTitle from './Chats/ChatTitle' +import { IAuthStore } from 'src/store/AuthStore'; const iconClose = require('../../images/iconClose.png') const sendActive = require('../../images/sendActive.png') @@ -62,11 +63,12 @@ type Props = { iconStore?: IconStore store?: IStore & IPersistable onceStore?: IOnceStore + authStore?: IAuthStore analytics?: any log?: any } -@inject('store', 'wocky', 'locationStore', 'iconStore', 'analytics', 'navStore', 'log', 'onceStore') +@inject('store', 'wocky', 'locationStore', 'iconStore', 'analytics', 'navStore', 'log', 'onceStore', 'authStore') @observer class TinyRobotRouter extends React.Component { componentDidMount() { @@ -98,19 +100,19 @@ class TinyRobotRouter extends React.Component { } render() { - const {store, iconStore, wocky, navStore, onceStore} = this.props + const {store, iconStore, wocky, navStore, onceStore, authStore} = this.props return ( navStore!.setScene(Actions.currentScene)} {...navBarStyle} uriPrefix={uriPrefix} onDeepLink={this.onDeepLink}> - wocky!.username && wocky!.password && wocky!.host} success="checkProfile" failure="preConnection" /> - + wocky!.username && wocky!.host} success="checkProfile" failure="preConnection" /> + wocky!.profile} success="checkHandle" failure="connect" /> wocky!.profile!.handle} success="checkOnboarded" failure="signUp" /> onceStore!.onboarded} success="logged" failure="onboarding" /> - + diff --git a/src/components/TestRegister.tsx b/src/components/TestRegister.tsx index b0678cab8..880538fa5 100644 --- a/src/components/TestRegister.tsx +++ b/src/components/TestRegister.tsx @@ -5,7 +5,8 @@ import {Actions} from 'react-native-router-flux' import {k, width} from './Global' import {colors} from '../constants' import {INavStore} from '../store/NavStore' -import {IWocky, IBypassStore} from 'wocky-client' +import {IWocky} from 'wocky-client' +import {IAuthStore} from 'src/store/AuthStore' type Props = { wocky?: IWocky @@ -13,14 +14,14 @@ type Props = { navStore?: INavStore name: string warn?: any - bypassStore?: IBypassStore + authStore: IAuthStore } type State = { text: string } -@inject('wocky', 'analytics', 'warn', 'navStore', 'bypassStore') +@inject('wocky', 'analytics', 'warn', 'navStore', 'authStore') @observer class TestRegister extends React.Component { state: State = { @@ -32,8 +33,8 @@ class TestRegister extends React.Component { return } - this.props.bypassStore!.setPhone(`+1555${this.state.text}`) - Actions.connect({providerName: this.props.bypassStore!.providerName}) + this.props.authStore!.register(`+1555${this.state.text}`, false) + Actions.connect() } render() { diff --git a/src/store/AuthStore.ts b/src/store/AuthStore.ts new file mode 100644 index 000000000..14f5db70c --- /dev/null +++ b/src/store/AuthStore.ts @@ -0,0 +1,68 @@ +import {types, Instance, getParent, flow} from 'mobx-state-tree' +import {Credentials} from 'wocky-client' +import analytics from 'src/utils/analytics' + +const AuthStore = types + .model('AuthStore', { + phone: types.maybe(types.string), + isFirebase: types.maybe(types.boolean), + }) + .views(self => ({ + get canLogin(): boolean { + return !!self.phone && self.isFirebase !== undefined + }, + })) + .actions(self => { + const {firebaseStore, wocky, homeStore, locationStore} = getParent(self) + + return { + setPhone: (phone: string) => (self.phone = phone), + + setIsFirebase: (isFirebase: boolean) => (self.isFirebase = isFirebase), + + register: (phone: string, isFirebase: boolean) => { + self.phone = phone + self.isFirebase = isFirebase + }, + + login: flow(function*() { + try { + let credentials: Credentials + + if (!self.canLogin) { + throw new Error('Phone number and provider must be set before login.') + } + + if (self.isFirebase) { + credentials = { + phone_number: self.phone, + ...(yield firebaseStore.getLoginCredentials()), + } + } else { + // bypass login + // Since users need to have unique `sub`s so we'll just use phoneNumber in the case of a bypass login + // https://hippware.slack.com/archives/C033TRJDD/p1543459452073900 + credentials = {typ: 'bypass', sub: self.phone!, phone_number: self.phone} + } + return wocky.login(credentials) + } catch (error) { + analytics.track('error_connection', {error}) + } + }), + + logout(): Promise { + homeStore.onLogout() + locationStore.onLogout() + if (self.isFirebase) { + firebaseStore.logout() + } + self.phone = undefined + self.isFirebase = undefined + return wocky.logout() + }, + } + }) + +export default AuthStore + +export interface IAuthStore extends Instance {} diff --git a/src/store/FirebaseStore.ts b/src/store/FirebaseStore.ts index 3ebeb359c..ecfd9f267 100644 --- a/src/store/FirebaseStore.ts +++ b/src/store/FirebaseStore.ts @@ -1,32 +1,32 @@ import {types, getEnv, flow, getParent} from 'mobx-state-tree' import {when} from 'mobx' -import {IWocky, registerProvider} from 'wocky-client' +import {IWocky, Credentials} from 'wocky-client' import {IEnv} from '.' import {settings} from '../globals' +import {RNFirebase} from 'react-native-firebase' +import {IAuthStore} from './AuthStore' type State = { - phone?: string - token?: string resource?: string buttonText?: string registered?: boolean errorMessage?: string + token?: string } const codeUrlString = '?inviteCode=' const FirebaseStore = types .model('FirebaseStore', { - phone: '', - providerName: 'firebase', - token: types.maybeNull(types.string), resource: types.maybeNull(types.string), inviteCode: types.maybeNull(types.string), }) .volatile(() => ({ + phone: '', buttonText: 'Verify', registered: false, errorMessage: '', // to avoid strange typescript errors when set it to string or null, + token: types.maybe(types.string), })) .actions(self => ({ setState(state: State) { @@ -43,6 +43,7 @@ const FirebaseStore = types })) .actions(self => { const {firebase, auth, logger, analytics}: IEnv = getEnv(self) + const {authStore} = getParent(self) let wocky: IWocky let confirmResult: any let unsubscribe: any @@ -59,7 +60,6 @@ const FirebaseStore = types } function afterAttach() { - registerProvider(self as any) auth.onAuthStateChanged(processFirebaseAuthChange) wocky = (getParent(self) as any).wocky // wocky could be null for HMR (?) // setup dynamic links @@ -97,9 +97,10 @@ const FirebaseStore = types if (user) { try { await auth!.currentUser!.reload() - const token = await auth!.currentUser!.getIdToken(true) - self.setState({token}) - // await firebase.auth().currentUser.updateProfile({phoneNumber: user.providerData[0].phoneNumber, displayName: '123'});) + // self.token = await auth!.currentUser!.getIdToken(true) + self.setState({ + token: await auth!.currentUser!.getIdToken(true), + }) } catch (err) { logger.warn('Firebase onAuthStateChanged error:', err) analytics.track('auth_error_firebase', {error: err}) @@ -113,27 +114,23 @@ const FirebaseStore = types } const getLoginCredentials = flow(function*() { - if (self.token) { - // Refresh firebase token if less than 5 minutes from expiry - const tokenResult = yield auth.currentUser!.getIdTokenResult(false) - if ( - tokenResult && - tokenResult.claims.exp && - tokenResult.claims.exp - Date.now() / 1000 < 300 - ) { - self.token = yield auth.currentUser!.getIdToken(true) - } - - return {typ: 'firebase', sub: self.token} - } else { - return {} + if (!self.token) { + return null } - }) + // Refresh firebase token if less than 5 minutes from expiry + const tokenResult: RNFirebase.IdTokenResult = yield auth.currentUser!.getIdTokenResult(false) + // todo: use moment instead of Date + if (tokenResult.claims.exp - Date.now() / 1000 < 300) { + self.token = yield auth.currentUser!.getIdToken(true) + } + + return {typ: 'firebase', sub: self.token, phone_number: self.phone} + }) as () => Promise - const onLogout = flow(function*() { + const logout = flow(function*() { analytics.track('logout') if (self.token) { - self.token = null + self.setState({token: undefined}) try { yield auth.signOut() } catch (err) { @@ -212,7 +209,8 @@ const FirebaseStore = types const registerWithToken = flow(function*() { try { self.setState({buttonText: 'Connecting...'}) - yield wocky.login(self.providerName) + authStore.register(self.phone, true) + yield (authStore as IAuthStore).login() self.setState({buttonText: 'Verify', registered: true}) } catch (err) { logger.warn('RegisterWithToken error', err) @@ -221,6 +219,7 @@ const FirebaseStore = types } finally { self.setState({ buttonText: 'Verify', + // todo: shouldn't overwrite errorMessage here errorMessage: '', }) } @@ -265,7 +264,7 @@ const FirebaseStore = types return { afterAttach, getLoginCredentials, - onLogout, + logout, beforeDestroy, verifyPhone, confirmCode, diff --git a/src/store/LocationStore.ts b/src/store/LocationStore.ts index fac9e15ed..c93d9c8ef 100644 --- a/src/store/LocationStore.ts +++ b/src/store/LocationStore.ts @@ -6,7 +6,6 @@ import DeviceInfo from 'react-native-device-info' import {settings} from '../globals' import {Location, IWocky} from 'wocky-client' import _ from 'lodash' -import {IStore} from '.' export const BG_STATE_PROPS = [ 'elasticityMultiplier', @@ -299,7 +298,8 @@ const LocationStore = types } }) .actions(self => { - const {wocky, onceStore} = getRoot(self) + // const {wocky, onceStore} = getRoot(self) + const {wocky, onceStore} = getRoot(self) let reactions: IReactionDisposer[] = [] const {logger} = getEnv(self) diff --git a/src/store/PersistableModel.ts b/src/store/PersistableModel.ts index 2dc39b56e..88b30efde 100644 --- a/src/store/PersistableModel.ts +++ b/src/store/PersistableModel.ts @@ -5,7 +5,7 @@ import {settings} from '../globals' export const cleanState = { firebaseStore: {}, - bypassStore: {}, + authStore: {}, locationStore: {}, searchStore: {}, profileValidationStore: {}, @@ -36,8 +36,7 @@ const PersistableModel = types function loadMinimal(parsed: any) { logger.log('loadMinimal') try { - const {username, password, host} = parsed.wocky - applySnapshot(self.wocky, {username, password, host}) + applySnapshot(self, {authStore: parsed.authStore} as any) } catch (err) { logger.warn('Minimal hydration error', err) analytics.track('loadMinimal_fail', parsed) @@ -80,6 +79,7 @@ const PersistableModel = types try { const data = await loadFromStorage(modelName) parsed = JSON.parse(data) + // console.log('& parsed', parsed) // throw new Error('Hydrate minimally') const pendingCodepush = parsed && parsed.codePushStore && parsed.codePushStore.pendingUpdate const newBinaryVersion = diff --git a/src/store/index.ts b/src/store/index.ts index d73a37acb..cfdccc2f7 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,9 +1,9 @@ -import {types, getEnv, addMiddleware, flow, Instance} from 'mobx-state-tree' +import {types, getEnv, addMiddleware, Instance} from 'mobx-state-tree' import {simpleActionLogger} from 'mst-middlewares' import {AsyncStorage} from 'react-native' import firebase, {RNFirebase, Firebase} from 'react-native-firebase' import DeviceInfo from 'react-native-device-info' -import {Wocky, Transport, AppInfo, IAppInfo} from 'wocky-client' +import {Transport, AppInfo, IAppInfo} from 'wocky-client' import nativeEnv from 'react-native-native-env' import {settings} from '../globals' @@ -11,7 +11,7 @@ import * as logger from '../utils/log' import analytics, {Analytics} from '../utils/analytics' import PersistableModel from './PersistableModel' import FirebaseStore from './FirebaseStore' -import {BypassStore} from 'wocky-client' +import AuthStore from './AuthStore' import fileService from './fileService' import LocationStore from './LocationStore' import SearchStore from './SearchStore' @@ -79,7 +79,7 @@ const Store = types .model('Store', { homeStore: HomeStore, firebaseStore: FirebaseStore, - bypassStore: BypassStore, + authStore: AuthStore, locationStore: LocationStore, searchStore: SearchStore, profileValidationStore: ProfileValidationStore, @@ -92,29 +92,17 @@ const Store = types return getEnv(self).fileService.getImageSize }, })) + +const PersistableStore = types + .compose(PersistableModel, Store) + .named(STORE_NAME) .actions(self => ({ - login: flow(function*(data) { - try { - yield self.wocky.login(data.providerName) - return true - } catch (error) { - analytics.track('error_connection', {error}) - } - return false - }), - logout: flow(function*() { - self.homeStore.onLogout() - self.locationStore.onLogout() - return yield self.wocky.logout() - }), afterCreate() { analytics.identify(self.wocky) }, })) -const PersistableStore = types.compose(PersistableModel, Store).named(STORE_NAME) - -export interface IStore extends Instance {} +export interface IStore extends Instance {} const theStore = PersistableStore.create( { diff --git a/third-party/wocky-client/src/index.ts b/third-party/wocky-client/src/index.ts index 59150c38d..1ccfe2838 100644 --- a/third-party/wocky-client/src/index.ts +++ b/third-party/wocky-client/src/index.ts @@ -26,4 +26,3 @@ export {OwnProfile, IOwnProfile} from './model/OwnProfile' export {Transport} from './transport/Transport' export {IPagingList} from './transport/types' export {AppInfo, IAppInfo} from './store/AppInfo' -export {BypassStore, IBypassStore} from './store/BypassStore' diff --git a/third-party/wocky-client/src/store/Wocky.ts b/third-party/wocky-client/src/store/Wocky.ts index f875f8d5f..55e4dd809 100644 --- a/third-party/wocky-client/src/store/Wocky.ts +++ b/third-party/wocky-client/src/store/Wocky.ts @@ -30,7 +30,6 @@ export const Wocky = types id: 'wocky', // todo: make this volatile? username: types.maybeNull(types.string), - phoneNumber: types.maybe(types.string), host: types.string, sessionCount: 0, profile: types.maybeNull(OwnProfile), diff --git a/third-party/wocky-client/test/support/testuser.ts b/third-party/wocky-client/test/support/testuser.ts index 08435be05..835ae9fdb 100644 --- a/third-party/wocky-client/test/support/testuser.ts +++ b/third-party/wocky-client/test/support/testuser.ts @@ -1,4 +1,4 @@ -import {Wocky, IWocky, Transport, BypassStore, registerProvider} from '../../src' +import {Wocky, IWocky, Transport} from '../../src' import {AppInfo} from '../../src/store/AppInfo' import fileService from './fileService' import {simpleActionLogger} from 'mst-middlewares' From b78f132351eebe71de663d0a2e0b586ce015332b Mon Sep 17 00:00:00 2001 From: Eric Kirkham Date: Mon, 21 Jan 2019 12:54:56 -0800 Subject: [PATCH 20/26] separate auth strategies from auth store --- src/store/AuthStore.ts | 56 ++++++++++++++----------------------- src/store/authStrategies.ts | 37 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 35 deletions(-) create mode 100644 src/store/authStrategies.ts diff --git a/src/store/AuthStore.ts b/src/store/AuthStore.ts index 14f5db70c..35af374af 100644 --- a/src/store/AuthStore.ts +++ b/src/store/AuthStore.ts @@ -1,64 +1,50 @@ -import {types, Instance, getParent, flow} from 'mobx-state-tree' -import {Credentials} from 'wocky-client' +import {types, Instance, getParent} from 'mobx-state-tree' import analytics from 'src/utils/analytics' +import strategies, {AuthStrategy, Strategy} from './authStrategies' const AuthStore = types .model('AuthStore', { phone: types.maybe(types.string), - isFirebase: types.maybe(types.boolean), + strategyName: types.optional(types.enumeration(['firebase', 'bypass']), 'firebase'), }) .views(self => ({ get canLogin(): boolean { - return !!self.phone && self.isFirebase !== undefined + return !!self.phone }, })) .actions(self => { - const {firebaseStore, wocky, homeStore, locationStore} = getParent(self) + const store = getParent(self) + const {wocky, homeStore, locationStore} = store + let strategy: AuthStrategy | null = null return { - setPhone: (phone: string) => (self.phone = phone), - - setIsFirebase: (isFirebase: boolean) => (self.isFirebase = isFirebase), - - register: (phone: string, isFirebase: boolean) => { + register: (phone: string, s: Strategy) => { self.phone = phone - self.isFirebase = isFirebase + self.strategyName = s }, - login: flow(function*() { + login(): Promise { try { - let credentials: Credentials - if (!self.canLogin) { - throw new Error('Phone number and provider must be set before login.') + throw new Error('Phone number must be set before login.') } - - if (self.isFirebase) { - credentials = { - phone_number: self.phone, - ...(yield firebaseStore.getLoginCredentials()), - } - } else { - // bypass login - // Since users need to have unique `sub`s so we'll just use phoneNumber in the case of a bypass login - // https://hippware.slack.com/archives/C033TRJDD/p1543459452073900 - credentials = {typ: 'bypass', sub: self.phone!, phone_number: self.phone} - } - return wocky.login(credentials) + strategy = strategies[self.strategyName] + return strategy.login(store) } catch (error) { - analytics.track('error_connection', {error}) + analytics.track('& error_connection', {error}) } - }), + return Promise.resolve(false) + }, logout(): Promise { homeStore.onLogout() locationStore.onLogout() - if (self.isFirebase) { - firebaseStore.logout() + if (strategy!.logout(store)) { + self.phone = undefined + self.strategyName = 'firebase' + return wocky.logout() } - self.phone = undefined - self.isFirebase = undefined - return wocky.logout() + return Promise.resolve(false) }, } }) diff --git a/src/store/authStrategies.ts b/src/store/authStrategies.ts new file mode 100644 index 000000000..9a6561f82 --- /dev/null +++ b/src/store/authStrategies.ts @@ -0,0 +1,37 @@ +import {Credentials} from 'wocky-client' + +export type AuthStrategy = { + login: (store) => Promise + logout: (store) => Promise +} + +export type Strategy = 'firebase' | 'bypass' + +interface IStrategies { + [key: string]: AuthStrategy +} + +const Strategies: IStrategies = { + firebase: { + login: async store => { + const {wocky, firebaseStore} = store + const credentials: Credentials = await firebaseStore.getLoginCredentials() + return wocky.login(credentials) + }, + logout: async store => { + return store.firebaseStore.logout() + }, + }, + bypass: { + login: async store => { + const {authStore: {phone}, wocky} = store + // Since users need to have unique `sub`s so we'll just use phoneNumber in the case of a bypass login + // https://hippware.slack.com/archives/C033TRJDD/p1543459452073900 + const credentials: Credentials = {typ: 'bypass', sub: phone, phone_number: phone} + return wocky.login(credentials) + }, + logout: () => Promise.resolve(true), + }, +} + +export default Strategies From 395294f4e8fa43945a654f6b857e9cabb1b40b8d Mon Sep 17 00:00:00 2001 From: Eric Kirkham Date: Mon, 21 Jan 2019 12:56:07 -0800 Subject: [PATCH 21/26] cleanup connectivity. add todos --- src/components/Connectivity.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/Connectivity.tsx b/src/components/Connectivity.tsx index 69f4dbda9..2d3dcf650 100644 --- a/src/components/Connectivity.tsx +++ b/src/components/Connectivity.tsx @@ -35,6 +35,7 @@ export default class Connectivity extends React.Component { this.props.log('NETINFO INITIAL:', reach, {level: log.levels.INFO}) this._handleConnectionInfoChange(reach) }) + // todo: refactor. Since interval is hardcoded here exponential backoff won't work...it checks every second regardless of changes to retryDelay this.intervalId = setInterval(async () => { const model = this.props.wocky! if ( @@ -70,13 +71,7 @@ export default class Connectivity extends React.Component { const info = {reason, currentState: AppState.currentState} const model = this.props.wocky! const {authStore} = this.props - if ( - AppState.currentState === 'active' && - !model.connected && - !model.connecting && - authStore!.canLogin && - model.host - ) { + if (AppState.currentState === 'active' && !model.connected && !model.connecting) { try { this.props.analytics.track('reconnect_try', { ...info, @@ -88,6 +83,7 @@ export default class Connectivity extends React.Component { this.retryDelay = 1000 } catch (e) { this.props.analytics.track('reconnect_fail', {...info, error: e}) + // todo: error message will be different with GraphQL (?) if (e.toString().indexOf('not-authorized') !== -1 || e.toString().indexOf('invalid')) { this.retryDelay = 1e9 Actions.logout() From 4b439010f9e4cb8581e3c53007f28f48ae04270e Mon Sep 17 00:00:00 2001 From: Eric Kirkham Date: Mon, 21 Jan 2019 12:57:49 -0800 Subject: [PATCH 22/26] cleanup --- src/components/Router.tsx | 2 +- src/components/TestRegister.tsx | 2 +- src/store/AuthStore.ts | 1 + src/store/FirebaseStore.ts | 6 +++--- third-party/wocky-client/src/store/Wocky.ts | 1 - 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Router.tsx b/src/components/Router.tsx index 22ec7a47d..c33f65a74 100644 --- a/src/components/Router.tsx +++ b/src/components/Router.tsx @@ -107,7 +107,7 @@ class TinyRobotRouter extends React.Component { - wocky!.username && wocky!.host} success="checkProfile" failure="preConnection" /> + authStore!.canLogin} success="checkProfile" failure="preConnection" /> wocky!.profile} success="checkHandle" failure="connect" /> wocky!.profile!.handle} success="checkOnboarded" failure="signUp" /> diff --git a/src/components/TestRegister.tsx b/src/components/TestRegister.tsx index 880538fa5..3108e6c95 100644 --- a/src/components/TestRegister.tsx +++ b/src/components/TestRegister.tsx @@ -33,7 +33,7 @@ class TestRegister extends React.Component { return } - this.props.authStore!.register(`+1555${this.state.text}`, false) + this.props.authStore!.register(`+1555${this.state.text}`, 'bypass') Actions.connect() } diff --git a/src/store/AuthStore.ts b/src/store/AuthStore.ts index 35af374af..35ac6009a 100644 --- a/src/store/AuthStore.ts +++ b/src/store/AuthStore.ts @@ -42,6 +42,7 @@ const AuthStore = types if (strategy!.logout(store)) { self.phone = undefined self.strategyName = 'firebase' + strategy = null return wocky.logout() } return Promise.resolve(false) diff --git a/src/store/FirebaseStore.ts b/src/store/FirebaseStore.ts index ecfd9f267..395295428 100644 --- a/src/store/FirebaseStore.ts +++ b/src/store/FirebaseStore.ts @@ -20,13 +20,13 @@ const FirebaseStore = types .model('FirebaseStore', { resource: types.maybeNull(types.string), inviteCode: types.maybeNull(types.string), + token: types.maybeNull(types.string), }) .volatile(() => ({ phone: '', buttonText: 'Verify', registered: false, errorMessage: '', // to avoid strange typescript errors when set it to string or null, - token: types.maybe(types.string), })) .actions(self => ({ setState(state: State) { @@ -130,7 +130,7 @@ const FirebaseStore = types const logout = flow(function*() { analytics.track('logout') if (self.token) { - self.setState({token: undefined}) + self.token = null try { yield auth.signOut() } catch (err) { @@ -209,7 +209,7 @@ const FirebaseStore = types const registerWithToken = flow(function*() { try { self.setState({buttonText: 'Connecting...'}) - authStore.register(self.phone, true) + authStore.register(self.phone, 'firebase') yield (authStore as IAuthStore).login() self.setState({buttonText: 'Verify', registered: true}) } catch (err) { diff --git a/third-party/wocky-client/src/store/Wocky.ts b/third-party/wocky-client/src/store/Wocky.ts index 55e4dd809..a666137f0 100644 --- a/third-party/wocky-client/src/store/Wocky.ts +++ b/third-party/wocky-client/src/store/Wocky.ts @@ -28,7 +28,6 @@ export const Wocky = types Storages, types.model({ id: 'wocky', - // todo: make this volatile? username: types.maybeNull(types.string), host: types.string, sessionCount: 0, From 38eac1bfc1043742529fc367fb9aae3fa1c6c51a Mon Sep 17 00:00:00 2001 From: Eric Kirkham Date: Mon, 21 Jan 2019 13:03:39 -0800 Subject: [PATCH 23/26] fix persistableModel --- src/store/PersistableModel.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/store/PersistableModel.ts b/src/store/PersistableModel.ts index 88b30efde..93650a8f7 100644 --- a/src/store/PersistableModel.ts +++ b/src/store/PersistableModel.ts @@ -1,6 +1,6 @@ import {types, getType, flow, getSnapshot, applySnapshot, getEnv, Instance} from 'mobx-state-tree' import {reaction} from 'mobx' -import {Wocky} from 'wocky-client' +import {Wocky, IWocky} from 'wocky-client' import {settings} from '../globals' export const cleanState = { @@ -34,9 +34,10 @@ const PersistableModel = types } function loadMinimal(parsed: any) { - logger.log('loadMinimal') + logger.log('loadMinimal', parsed) try { - applySnapshot(self, {authStore: parsed.authStore} as any) + // todo: try rehydrating onceStore to prevent going through onboarding after a cache reset? + applySnapshot((self as any).authStore, parsed.authStore) } catch (err) { logger.warn('Minimal hydration error', err) analytics.track('loadMinimal_fail', parsed) @@ -91,7 +92,7 @@ const PersistableModel = types applySnapshot(self, parsed) } } catch (err) { - logger.log('hydration error', modelName, parsed, err) + logger.log('hydration error', modelName, err, parsed) if (modelName === STORE_NAME && parsed && parsed.wocky) { loadMinimal(parsed) } else { @@ -132,15 +133,17 @@ const PersistableModel = types disposePersistenceReaction() disposePersistenceReaction = undefined } + // calling self.wocky here causes ts problems so the following is necessary + const wocky: IWocky = self.wocky // shut down wocky - self.wocky.clearCache() - self.wocky.disposeReactions() + wocky.clearCache() + wocky.disposeReactions() // wipe out old state and apply clean applySnapshot(self as any, { ...cleanState, wocky: {host: settings.getDomain()}, }) - self.wocky.startReactions() + wocky.startReactions() // load minimal state to re-login const data = yield loadFromStorage(STORE_NAME) const parsed = JSON.parse(data) From 564c1da4f89922b4659d8034394fb1053ba347f2 Mon Sep 17 00:00:00 2001 From: Eric Kirkham Date: Mon, 21 Jan 2019 14:23:59 -0800 Subject: [PATCH 24/26] fix connectivity (prevent infinite retries) --- src/components/Connectivity.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Connectivity.tsx b/src/components/Connectivity.tsx index 2d3dcf650..f865009d7 100644 --- a/src/components/Connectivity.tsx +++ b/src/components/Connectivity.tsx @@ -42,6 +42,7 @@ export default class Connectivity extends React.Component { this.isActive && !model.connected && !model.connecting && + this.props.authStore!.canLogin && Date.now() - this.lastDisconnected >= this.retryDelay ) { await this.tryReconnect(`retry: ${this.retryDelay}`) From d2026f9586eb163a4f798fc20fd2db668e05b191 Mon Sep 17 00:00:00 2001 From: Eric Kirkham Date: Mon, 21 Jan 2019 14:24:35 -0800 Subject: [PATCH 25/26] fix logout --- src/store/AuthStore.ts | 22 +++++++++++----------- src/store/HomeStore.ts | 2 +- src/store/LocationStore.ts | 4 ++-- src/store/authStrategies.ts | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/store/AuthStore.ts b/src/store/AuthStore.ts index 35ac6009a..c59a54af6 100644 --- a/src/store/AuthStore.ts +++ b/src/store/AuthStore.ts @@ -1,4 +1,4 @@ -import {types, Instance, getParent} from 'mobx-state-tree' +import {types, Instance, getParent, flow, applySnapshot} from 'mobx-state-tree' import analytics from 'src/utils/analytics' import strategies, {AuthStrategy, Strategy} from './authStrategies' @@ -36,17 +36,17 @@ const AuthStore = types return Promise.resolve(false) }, - logout(): Promise { - homeStore.onLogout() - locationStore.onLogout() - if (strategy!.logout(store)) { - self.phone = undefined - self.strategyName = 'firebase' - strategy = null - return wocky.logout() + logout: flow(function*() { + homeStore.logout() + locationStore.logout() + if (strategy) { + yield strategy.logout(store) } - return Promise.resolve(false) - }, + applySnapshot(self, {}) + strategy = null + yield wocky.logout() + return true + }), } }) diff --git a/src/store/HomeStore.ts b/src/store/HomeStore.ts index 440a141b7..45652cac6 100644 --- a/src/store/HomeStore.ts +++ b/src/store/HomeStore.ts @@ -131,7 +131,7 @@ const HomeStore = types })) .actions(self => { return { - onLogout() { + logout() { applySnapshot(self, {}) }, addBotsToList(bots: IBot[]): void { diff --git a/src/store/LocationStore.ts b/src/store/LocationStore.ts index c93d9c8ef..bb0767d94 100644 --- a/src/store/LocationStore.ts +++ b/src/store/LocationStore.ts @@ -371,7 +371,7 @@ const LocationStore = types BackgroundGeolocation.un('providerchange', self.onProviderChange) } - const onLogout = flow(function*() { + const logout = flow(function*() { yield self.invalidateCredentials() yield BackgroundGeolocation.stop() logger.log(prefix, 'Stop') @@ -382,7 +382,7 @@ const LocationStore = types finish, didMount, willUnmount, - onLogout, + logout, } }) diff --git a/src/store/authStrategies.ts b/src/store/authStrategies.ts index 9a6561f82..1a517babd 100644 --- a/src/store/authStrategies.ts +++ b/src/store/authStrategies.ts @@ -2,7 +2,7 @@ import {Credentials} from 'wocky-client' export type AuthStrategy = { login: (store) => Promise - logout: (store) => Promise + logout: (store) => Promise } export type Strategy = 'firebase' | 'bypass' @@ -30,7 +30,7 @@ const Strategies: IStrategies = { const credentials: Credentials = {typ: 'bypass', sub: phone, phone_number: phone} return wocky.login(credentials) }, - logout: () => Promise.resolve(true), + logout: Promise.resolve, }, } From b88f9b01374904932e52d8b1cdf22173630ee802 Mon Sep 17 00:00:00 2001 From: Eric Kirkham Date: Mon, 21 Jan 2019 15:06:00 -0800 Subject: [PATCH 26/26] cleanup --- src/store/AuthStore.ts | 2 +- src/store/PersistableModel.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/store/AuthStore.ts b/src/store/AuthStore.ts index c59a54af6..ca3cb0586 100644 --- a/src/store/AuthStore.ts +++ b/src/store/AuthStore.ts @@ -31,7 +31,7 @@ const AuthStore = types strategy = strategies[self.strategyName] return strategy.login(store) } catch (error) { - analytics.track('& error_connection', {error}) + analytics.track('error_connection', {error}) } return Promise.resolve(false) }, diff --git a/src/store/PersistableModel.ts b/src/store/PersistableModel.ts index 93650a8f7..f74d83880 100644 --- a/src/store/PersistableModel.ts +++ b/src/store/PersistableModel.ts @@ -80,7 +80,6 @@ const PersistableModel = types try { const data = await loadFromStorage(modelName) parsed = JSON.parse(data) - // console.log('& parsed', parsed) // throw new Error('Hydrate minimally') const pendingCodepush = parsed && parsed.codePushStore && parsed.codePushStore.pendingUpdate const newBinaryVersion =