-
Notifications
You must be signed in to change notification settings - Fork 167
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(flag disabled) KYC registration (#2678)
* Basic layout for gidx registration integration * Add registration phone verification section * Add first pass at reason code handling * Remove unused * Get verification session script string * Get verification session script string * Upoad identity documents to gidx * Refactor * Get verification session script string * Fix import * Fix import * Tweak user flow * Comment * Request location - builds on ios, but not on android yet * Yarn clean - builds on ios and android * Fix 2fa login problem on android * Allow android to pull from top to refresh * Set end refresh on load end * Enable cache on webview * Fix urls * Add gidx registration enabled flag * Get last known location if known
- Loading branch information
1 parent
83b39a0
commit c31de72
Showing
74 changed files
with
4,004 additions
and
349 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { APIHandler } from 'api/helpers/endpoint' | ||
import { log } from 'shared/utils' | ||
|
||
export const callbackGIDX: APIHandler<'callback-gidx'> = async (props) => { | ||
log('callback-gidx', props) | ||
return { success: true } | ||
} | ||
|
||
const documentStatus = { | ||
1: 'Not Reviewed', | ||
2: 'Under Review', | ||
3: 'Review Complete', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { APIError, APIHandler } from 'api/helpers/endpoint' | ||
import { getPrivateUserSupabase, log } from 'shared/utils' | ||
import { getPhoneNumber } from 'shared/helpers/get-phone-number' | ||
import { getGIDXStandardParams } from 'shared/gidx/standard-params' | ||
import { | ||
GIDX_REGISTATION_ENABLED, | ||
GIDXVerificationResponse, | ||
} from 'common/gidx/gidx' | ||
const ENDPOINT = 'https://api.gidx-service.in/v3.0/api/WebReg/CreateSession' | ||
export const getVerificationSession: APIHandler< | ||
'get-verification-session-gidx' | ||
> = async (props, auth) => { | ||
if (!GIDX_REGISTATION_ENABLED) | ||
throw new APIError(400, 'GIDX registration is disabled') | ||
const user = await getPrivateUserSupabase(auth.uid) | ||
if (!user) { | ||
throw new APIError(404, 'Private user not found') | ||
} | ||
if (!user.email) { | ||
throw new APIError(400, 'User must have an email address') | ||
} | ||
const phoneNumberWithCode = await getPhoneNumber(auth.uid) | ||
if (!phoneNumberWithCode) { | ||
throw new APIError(400, 'User must have a phone number') | ||
} | ||
const body = { | ||
// TODO: add back in prod | ||
// MerchantCustomerID: auth.uid,, | ||
// EmailAddress: user.email, | ||
// MobilePhoneNumber: parsePhoneNumber(phoneNumberWithCode)?.nationalNumber ?? phoneNumberWithCode, | ||
// DeviceIpAddress: getIp(req), | ||
CustomerIpAddress: props.DeviceIpAddress, | ||
// CallbackURL: 'https://api.manifold.markets/v0/gidx/verification-callback', | ||
CallbackURL: | ||
'https://enabled-bream-sharply.ngrok-free.app/v0/callback-gidx', | ||
...getGIDXStandardParams(), | ||
...props, | ||
} | ||
log('Registration request:', body) | ||
const res = await fetch(ENDPOINT, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify(body), | ||
}) | ||
if (!res.ok) { | ||
throw new APIError(400, 'GIDX verification session failed') | ||
} | ||
|
||
const data = (await res.json()) as GIDXVerificationResponse | ||
log('Registration response:', data) | ||
return data | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { APIError, APIHandler } from 'api/helpers/endpoint' | ||
import { getPrivateUserSupabase, log } from 'shared/utils' | ||
import { getPhoneNumber } from 'shared/helpers/get-phone-number' | ||
import { getGIDXStandardParams } from 'shared/gidx/standard-params' | ||
import { GIDX_REGISTATION_ENABLED, GIDXDocument } from 'common/gidx/gidx' | ||
// TODO this endpoint returns a 404, the endpoint doesn't exist... | ||
const ENDPOINT = | ||
'https://api.gidx-service.in/v3.0/api/DocumentLibrary/CustomerDocument' | ||
|
||
export const getVerificationStatus: APIHandler< | ||
'get-verification-status-gidx' | ||
> = async (_, auth) => { | ||
if (!GIDX_REGISTATION_ENABLED) | ||
throw new APIError(400, 'GIDX registration is disabled') | ||
const user = await getPrivateUserSupabase(auth.uid) | ||
if (!user) { | ||
throw new APIError(404, 'Private user not found') | ||
} | ||
if (!user.email) { | ||
throw new APIError(400, 'User must have an email address') | ||
} | ||
const phoneNumberWithCode = await getPhoneNumber(auth.uid) | ||
if (!phoneNumberWithCode) { | ||
throw new APIError(400, 'User must have a phone number') | ||
} | ||
// TODO: Handle more than just check on their document upload. Let them know if they've failed, blocked, not yet started, etc. | ||
|
||
const body = { | ||
MerchantCustomerID: auth.uid, | ||
...getGIDXStandardParams(), | ||
} | ||
const queryParams = new URLSearchParams(body as any).toString() | ||
const urlWithParams = `${ENDPOINT}?${queryParams}` | ||
|
||
const res = await fetch(urlWithParams) | ||
if (!res.ok) { | ||
throw new APIError(400, 'GIDX verification session failed') | ||
} | ||
|
||
const data = (await res.json()) as { | ||
ResponseCode: number | ||
ResponseMessage: string | ||
MerchantCustomerID: string | ||
DocumentCount: number | ||
Documents: GIDXDocument[] | ||
} | ||
log( | ||
'Registration response:', | ||
data.ResponseMessage, | ||
'docs', | ||
data.DocumentCount, | ||
'userId', | ||
data.MerchantCustomerID | ||
) | ||
return { | ||
status: 'success', | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import { APIError, APIHandler } from 'api/helpers/endpoint' | ||
import { getPrivateUserSupabase, log } from 'shared/utils' | ||
import { getPhoneNumber } from 'shared/helpers/get-phone-number' | ||
import { updateUser } from 'shared/supabase/users' | ||
import { createSupabaseDirectClient } from 'shared/supabase/init' | ||
import { | ||
otherErrorCodes, | ||
hasIdentityError, | ||
blockedCodes, | ||
allowedFlaggedCodes, | ||
locationTemporarilyBlockedCodes, | ||
} from 'common/reason-codes' | ||
import { intersection } from 'lodash' | ||
import { getGIDXStandardParams } from 'shared/gidx/standard-params' | ||
import { GIDX_REGISTATION_ENABLED } from 'common/gidx/gidx' | ||
const ENDPOINT = | ||
'https://api.gidx-service.in/v3.0/api/CustomerIdentity/CustomerRegistration' | ||
|
||
export const register: APIHandler<'register-gidx'> = async ( | ||
props, | ||
auth, | ||
req | ||
) => { | ||
if (!GIDX_REGISTATION_ENABLED) | ||
throw new APIError(400, 'GIDX registration is disabled') | ||
const pg = createSupabaseDirectClient() | ||
const user = await getPrivateUserSupabase(auth.uid) | ||
if (!user) { | ||
throw new APIError(404, 'Private user not found') | ||
} | ||
if (!user.email) { | ||
throw new APIError(400, 'User must have an email address') | ||
} | ||
const phoneNumberWithCode = await getPhoneNumber(auth.uid) | ||
if (!phoneNumberWithCode) { | ||
throw new APIError(400, 'User must have a phone number') | ||
} | ||
const body = { | ||
// TODO: add back in prod | ||
// MerchantCustomerID: auth.uid,, | ||
// EmailAddress: user.email, | ||
// MobilePhoneNumber: parsePhoneNumber(phoneNumberWithCode)?.nationalNumber ?? phoneNumberWithCode, | ||
// DeviceIpAddress: getIp(req), | ||
...getGIDXStandardParams(), | ||
...props, | ||
} | ||
log('Registration request:', body) | ||
const res = await fetch(ENDPOINT, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify(body), | ||
}) | ||
if (!res.ok) { | ||
throw new APIError(400, 'GIDX registration failed') | ||
} | ||
|
||
const data = (await res.json()) as GIDXRegistrationResponse | ||
log('Registration response:', data) | ||
const { ReasonCodes, FraudConfidenceScore, IdentityConfidenceScore } = data | ||
|
||
// Timeouts or input errors | ||
const errorCodes = intersection(otherErrorCodes, ReasonCodes) | ||
if (errorCodes.length > 0) { | ||
log('Registration failed, resulted in error codes:', errorCodes) | ||
await updateUser(pg, auth.uid, { kycStatus: 'failed' }) | ||
return { status: 'error', ReasonCodes } | ||
} | ||
|
||
// User identity match is low confidence or attempt may be fraud | ||
if (FraudConfidenceScore < 50 || IdentityConfidenceScore < 50) { | ||
log( | ||
'Registration failed, resulted in low confidence scores:', | ||
FraudConfidenceScore, | ||
IdentityConfidenceScore | ||
) | ||
await updateUser(pg, auth.uid, { kycStatus: 'failed' }) | ||
return { | ||
status: 'error', | ||
ReasonCodes, | ||
FraudConfidenceScore, | ||
IdentityConfidenceScore, | ||
} | ||
} | ||
|
||
// User identity not found/verified | ||
if (hasIdentityError(ReasonCodes)) { | ||
log('Registration failed, resulted in identity errors:', ReasonCodes) | ||
await updateUser(pg, auth.uid, { kycStatus: 'failed' }) | ||
return { status: 'error', ReasonCodes } | ||
} else await updateUser(pg, auth.uid, { kycMatch: true }) | ||
|
||
// User is flagged for unknown address, vpn, unclear DOB, distance between attempts | ||
const allowedFlags = intersection(allowedFlaggedCodes, ReasonCodes) | ||
if (allowedFlags.length > 0) { | ||
await updateUser(pg, auth.uid, { kycFlags: allowedFlags }) | ||
} | ||
|
||
// User is in disallowed location, but they may move | ||
if (intersection(locationTemporarilyBlockedCodes, ReasonCodes).length > 0) { | ||
log( | ||
'Registration failed, resulted in temporary blocked location codes:', | ||
ReasonCodes | ||
) | ||
await updateUser(pg, auth.uid, { kycStatus: 'failed' }) | ||
return { status: 'error', ReasonCodes } | ||
} | ||
|
||
// User is blocked for any number of reasons | ||
const blockedReasonCodes = intersection(blockedCodes, ReasonCodes) | ||
if (blockedReasonCodes.length > 0) { | ||
log('Registration failed, resulted in blocked codes:', blockedReasonCodes) | ||
await updateUser(pg, auth.uid, { kycStatus: 'blocked' }) | ||
return { status: 'error', ReasonCodes } | ||
} | ||
|
||
// User is not blocked and ID is verified | ||
if (ReasonCodes.includes('ID-VERIFIED')) { | ||
log('Registration passed with allowed codes:', ReasonCodes) | ||
await updateUser(pg, auth.uid, { kycStatus: 'verified' }) | ||
return { status: 'success', ReasonCodes } | ||
} | ||
|
||
log.error( | ||
`Registration failed with unknown reason codes: ${ReasonCodes.join(', ')}` | ||
) | ||
return { status: 'error', ReasonCodes } | ||
} | ||
|
||
type GIDXRegistrationResponse = { | ||
MerchantCustomerID: string | ||
ReasonCodes: string[] | ||
WatchChecks: WatchCheckType[] | ||
ProfileMatch: ProfileMatchType | ||
IdentityConfidenceScore: number | ||
FraudConfidenceScore: number | ||
CustomerRegistrationLink: string | ||
LocationDetail: LocationDetailType | ||
ResponseCode: number | ||
ResponseMessage: string | ||
ProfileMatches: ProfileMatchType[] | ||
} | ||
|
||
type WatchCheckType = { | ||
SourceCode: string | ||
SourceScore: number | ||
MatchResult: boolean | ||
MatchScore: number | ||
} | ||
|
||
type ProfileMatchType = { | ||
NameMatch: boolean | ||
AddressMatch: boolean | ||
EmailMatch: boolean | ||
IdDocumentMatch: boolean | ||
PhoneMatch: boolean | ||
MobilePhoneMatch: boolean | ||
DateOfBirthMatch: boolean | ||
CitizenshipMatch: boolean | ||
} | ||
|
||
type LocationDetailType = { | ||
Latitude: number | ||
Longitude: number | ||
Radius: number | ||
Altitude: number | ||
Speed: number | ||
LocationDateTime: string | ||
LocationStatus: number | ||
LocationServiceLevel: string | ||
ReasonCodes: string[] | ||
ComplianceLocationStatus: boolean | ||
ComplianceLocationServiceStatus: string | ||
IdentifierType: string | ||
IdentifierUsed: string | ||
} |
Oops, something went wrong.