import dotProp from 'dot-prop-immutable' import { RehydrateAction } from 'redux-persist' import { Actions as AccountActions, ClearStoredAccountAction } from 'src/account/actions' import { ActionTypes, Actions } from 'src/identity/actions' import { ImportContactsStatus } from 'src/identity/types' import { REHYDRATE, getRehydratePayload } from 'src/redux/persist-helper' export interface AddressToE164NumberType { [address: string]: string | null } export interface E164NumberToAddressType { [e164PhoneNumber: string]: string[] | null | undefined // null means unverified } export interface AddressInfoToDisplay { name: string imageUrl: string | null isCeloRewardSender?: boolean isProviderAddress?: boolean } // This mapping is just for storing provider info from firebase // other known recipient should be stored in the appRecipientCache export interface AddressToDisplayNameType { [address: string]: AddressInfoToDisplay | undefined } export interface ImportContactProgress { status: ImportContactsStatus current: number total: number } export enum AddressValidationType { FULL = 'full', PARTIAL = 'partial', NONE = 'none', } export interface SecureSendPhoneNumberMapping { [e164Number: string]: SecureSendDetails } export interface SecureSendDetails { address?: string addressValidationType: AddressValidationType isFetchingAddresses?: boolean lastFetchSuccessful?: boolean validationSuccessful?: boolean } export interface AddressToVerificationStatus { [address: string]: boolean | undefined } interface State { addressToE164Number: AddressToE164NumberType // Note: Do not access values in this directly, use the `getAddressFromPhoneNumber` helper in contactMapping e164NumberToAddress: E164NumberToAddressType // Doesn't contain all known addresses, use only as a fallback. addressToDisplayName: AddressToDisplayNameType // Has the user already been asked for contacts permission askedContactsPermission: boolean importContactsProgress: ImportContactProgress // Contacts found during the matchmaking process secureSendPhoneNumberMapping: SecureSendPhoneNumberMapping // Mapping of address to verification status; undefined entries represent a loading state addressToVerificationStatus: AddressToVerificationStatus lastSavedContactsHash: string | null shouldRefreshStoredPasswordHash: boolean } const initialState: State = { addressToE164Number: {}, e164NumberToAddress: {}, addressToDisplayName: {}, askedContactsPermission: false, importContactsProgress: { status: ImportContactsStatus.Stopped, current: 0, total: 0, }, secureSendPhoneNumberMapping: {}, addressToVerificationStatus: {}, lastSavedContactsHash: null, shouldRefreshStoredPasswordHash: false, } export const reducer = ( state: State | undefined = initialState, action: ActionTypes | RehydrateAction | ClearStoredAccountAction ): State => { switch (action.type) { case REHYDRATE: { // Ignore some persisted properties const rehydratedState = getRehydratePayload(action, 'identity') return { ...state, ...rehydratedState, importContactsProgress: { status: ImportContactsStatus.Stopped, current: 0, total: 0, }, } } case Actions.UPDATE_E164_PHONE_NUMBER_ADDRESSES: return { ...state, addressToE164Number: { ...state.addressToE164Number, ...action.addressToE164Number }, e164NumberToAddress: { ...state.e164NumberToAddress, ...action.e164NumberToAddress, }, } case Actions.UPDATE_KNOWN_ADDRESSES: return { ...state, addressToDisplayName: { ...state.addressToDisplayName, ...action.knownAddresses, }, } case Actions.IMPORT_CONTACTS: return { ...state, askedContactsPermission: true, importContactsProgress: { status: ImportContactsStatus.Prepping, current: 0, total: 0 }, } case Actions.UPDATE_IMPORT_CONTACT_PROGRESS: const curProgress = state.importContactsProgress return { ...state, importContactsProgress: { current: action.current ?? curProgress.current, total: action.total ?? curProgress.total, status: action.status ?? curProgress.status, }, } case Actions.END_IMPORT_CONTACTS: const { success } = action return { ...state, importContactsProgress: { ...state.importContactsProgress, status: success ? ImportContactsStatus.Done : ImportContactsStatus.Failed, }, } case Actions.VALIDATE_RECIPIENT_ADDRESS_SUCCESS: return { ...state, // Overwrite the previous mapping when a new address is validated secureSendPhoneNumberMapping: dotProp.set( state.secureSendPhoneNumberMapping, `${action.e164Number}`, { address: action.validatedAddress, addressValidationType: AddressValidationType.NONE, validationSuccessful: true, } ), } case Actions.VALIDATE_RECIPIENT_ADDRESS_RESET: return { ...state, secureSendPhoneNumberMapping: dotProp.set( state.secureSendPhoneNumberMapping, `${action.e164Number}.validationSuccessful`, false ), } case Actions.REQUIRE_SECURE_SEND: return { ...state, // Erase the previous mapping when new validation is required secureSendPhoneNumberMapping: dotProp.set( state.secureSendPhoneNumberMapping, `${action.e164Number}`, { address: undefined, addressValidationType: action.addressValidationType, } ), } case Actions.FETCH_ADDRESSES_AND_VALIDATION_STATUS: return { ...state, secureSendPhoneNumberMapping: dotProp.set( state.secureSendPhoneNumberMapping, `${action.e164Number}.isFetchingAddresses`, true ), } case Actions.END_FETCHING_ADDRESSES: return { ...state, secureSendPhoneNumberMapping: dotProp.merge( state.secureSendPhoneNumberMapping, `${action.e164Number}`, { isFetchingAddresses: false, lastFetchSuccessful: action.lastFetchSuccessful } ), } case AccountActions.CLEAR_STORED_ACCOUNT: return { ...initialState, addressToE164Number: state.addressToE164Number, e164NumberToAddress: state.e164NumberToAddress, secureSendPhoneNumberMapping: state.secureSendPhoneNumberMapping, } case Actions.FETCH_ADDRESS_VERIFICATION_STATUS: // If the current status is false or does not exist, we set it to undefined // to mark it as being in a loading state. return { ...state, addressToVerificationStatus: { ...state.addressToVerificationStatus, [action.address]: state.addressToVerificationStatus[action.address] || undefined, }, } case Actions.ADDRESS_VERIFICATION_STATUS_RECEIVED: return { ...state, addressToVerificationStatus: { ...state.addressToVerificationStatus, [action.address]: action.addressVerified, }, } case Actions.CONTACTS_SAVED: return { ...state, lastSavedContactsHash: action.hash, } case Actions.STORED_PASSWORD_REFRESHED: return { ...state, shouldRefreshStoredPasswordHash: false, } default: return state } }