From c31de72758edae6bf1597862520881a14b3efd80 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Thu, 20 Jun 2024 17:10:35 -0700 Subject: [PATCH] (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 --- backend/api/package.json | 2 + backend/api/src/app.ts | 12 +- backend/api/src/get-phone-number.ts | 20 - backend/api/src/gidx/callback.ts | 13 + .../api/src/gidx/get-verification-session.ts | 54 + .../api/src/gidx/get-verification-status.ts | 58 + backend/api/src/gidx/register.ts | 177 ++ backend/api/src/gidx/upload-document.ts | 88 + backend/api/src/helpers/endpoint.ts | 5 + backend/api/src/verify-phone-number.ts | 8 +- backend/shared/src/gidx/standard-params.ts | 11 + .../shared/src/helpers/get-phone-number.ts | 11 + backend/shared/src/utils.ts | 9 + common/src/api/schema.ts | 51 +- common/src/economy.ts | 1 + common/src/envs/dev.ts | 1 + common/src/envs/prod.ts | 2 + common/src/gidx/gidx.ts | 67 + common/src/native-message.ts | 23 +- common/src/reason-codes.ts | 81 + common/src/secrets.ts | 5 + common/src/user.ts | 3 + native/App.tsx | 132 +- native/README.md | 10 +- native/android/.gitignore | 1 + native/android/app/build.gradle | 31 +- .../android/app/src/debug/AndroidManifest.xml | 2 +- .../android/app/src/main/AndroidManifest.xml | 6 +- .../java/com/markets/manifold/MainActivity.kt | 2 +- .../com/markets/manifold/MainApplication.kt | 2 +- .../res/drawable/rn_edit_text_material.xml | 5 +- .../markets/manifold/ReactNativeFlipper.java | 20 - native/android/build.gradle | 16 +- native/android/gradle.properties | 3 +- .../android/gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- native/android/gradlew | 8 +- native/android/gradlew.bat | 1 + native/android/settings.gradle | 3 +- native/components/auth-page.tsx | 2 +- native/components/custom-webview.tsx | 204 ++ native/components/web-view-utils.tsx | 72 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - native/ios/Manifold/AppDelegate.mm | 3 +- native/ios/Manifold/Info.plist | 6 + native/ios/Manifold/PrivacyInfo.xcprivacy | 48 + native/ios/Podfile | 16 +- native/ios/Podfile.lock | 1879 +++++++++++++++++ native/lib/location.ts | 22 + native/package.json | 1 + native/yarn.lock | 5 + private-storage.rules | 10 + web/components/add-funds-modal.tsx | 8 +- web/components/buttons/file-upload-button.tsx | 12 +- web/components/buy-mana-button.tsx | 8 +- .../contract/change-banner-button.tsx | 5 +- web/components/country-code-selector.tsx | 79 + web/components/editor/upload-extension.tsx | 4 +- web/components/gidx/register-user-form.tsx | 505 +++++ web/components/gidx/upload-document.tsx | 136 ++ web/components/gidx/verify-me.tsx | 60 + web/components/native-message-listener.tsx | 74 +- ...-phone.tsx => onboarding-verify-phone.tsx} | 2 +- web/components/onboarding/welcome.tsx | 6 +- web/components/registration-verify-phone.tsx | 122 ++ .../user/verify-phone-number-banner.tsx | 4 +- web/hooks/use-native-messages.ts | 8 +- web/lib/firebase/init.ts | 6 + web/lib/firebase/storage.ts | 32 +- web/package.json | 2 + web/pages/[username]/index.tsx | 2 + web/pages/gidx/register.tsx | 17 + web/pages/profile.tsx | 4 +- yarn.lock | 33 +- 74 files changed, 4004 insertions(+), 349 deletions(-) delete mode 100644 backend/api/src/get-phone-number.ts create mode 100644 backend/api/src/gidx/callback.ts create mode 100644 backend/api/src/gidx/get-verification-session.ts create mode 100644 backend/api/src/gidx/get-verification-status.ts create mode 100644 backend/api/src/gidx/register.ts create mode 100644 backend/api/src/gidx/upload-document.ts create mode 100644 backend/shared/src/gidx/standard-params.ts create mode 100644 backend/shared/src/helpers/get-phone-number.ts create mode 100644 common/src/gidx/gidx.ts create mode 100644 common/src/reason-codes.ts delete mode 100644 native/android/app/src/release/java/com/markets/manifold/ReactNativeFlipper.java create mode 100644 native/components/custom-webview.tsx delete mode 100644 native/components/web-view-utils.tsx delete mode 100644 native/ios/Manifold.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 native/ios/Manifold/PrivacyInfo.xcprivacy create mode 100644 native/ios/Podfile.lock create mode 100644 native/lib/location.ts create mode 100644 private-storage.rules create mode 100644 web/components/country-code-selector.tsx create mode 100644 web/components/gidx/register-user-form.tsx create mode 100644 web/components/gidx/upload-document.tsx create mode 100644 web/components/gidx/verify-me.tsx rename web/components/{verify-phone.tsx => onboarding-verify-phone.tsx} (98%) create mode 100644 web/components/registration-verify-phone.tsx create mode 100644 web/pages/gidx/register.tsx diff --git a/backend/api/package.json b/backend/api/package.json index f4aaab766c..59c6e16f78 100644 --- a/backend/api/package.json +++ b/backend/api/package.json @@ -40,8 +40,10 @@ "expo-server-sdk": "3.6.0", "express": "4.18.1", "firebase-admin": "11.11.1", + "form-data": "4.0.0", "gcp-metadata": "6.1.0", "jsonwebtoken": "9.0.0", + "libphonenumber-js": "1.11.3", "link-preview-js": "3.0.4", "lodash": "4.17.21", "mailgun-js": "0.22.0", diff --git a/backend/api/src/app.ts b/backend/api/src/app.ts index e6d8e3552d..a7d65d1daa 100644 --- a/backend/api/src/app.ts +++ b/backend/api/src/app.ts @@ -151,7 +151,6 @@ import { getUserPortfolio } from './get-user-portfolio' import { createuser } from 'api/create-user' import { verifyPhoneNumber } from 'api/verify-phone-number' import { requestOTP } from 'api/request-phone-otp' -import { getPhoneNumber } from 'api/get-phone-number' import { multiSell } from 'api/multi-sell' import { convertSpiceToMana } from './convert-sp-to-mana' import { donate } from './donate' @@ -167,6 +166,11 @@ import { blockGroup, unblockGroup } from './block-group' import { blockMarket, unblockMarket } from './block-market' import { getTxnSummaryStats } from 'api/get-txn-summary-stats' import { getManaSummaryStats } from 'api/get-mana-summary-stats' +import { register } from 'api/gidx/register' +import { getVerificationSession } from 'api/gidx/get-verification-session' +import { uploadDocument } from 'api/gidx/upload-document' +import { callbackGIDX } from 'api/gidx/callback' +import { getVerificationStatus } from 'api/gidx/get-verification-status' import { getCurrentPrivateUser } from './get-current-private-user' import { updatePrivateUser } from './update-private-user' import { setPushToken } from './push-token' @@ -331,7 +335,6 @@ const handlers: { [k in APIPath]: APIHandler } = { createuser: createuser, 'verify-phone-number': verifyPhoneNumber, 'request-otp': requestOTP, - 'phone-number': getPhoneNumber, 'multi-sell': multiSell, 'get-feed': getFeed, 'get-mana-supply': getManaSupply, @@ -340,6 +343,11 @@ const handlers: { [k in APIPath]: APIHandler } = { 'search-contract-positions': searchContractPositions, 'get-txn-summary-stats': getTxnSummaryStats, 'get-mana-summary-stats': getManaSummaryStats, + 'register-gidx': register, + 'get-verification-session-gidx': getVerificationSession, + 'get-verification-status-gidx': getVerificationStatus, + 'upload-document-gidx': uploadDocument, + 'callback-gidx': callbackGIDX, } Object.entries(handlers).forEach(([path, handler]) => { diff --git a/backend/api/src/get-phone-number.ts b/backend/api/src/get-phone-number.ts deleted file mode 100644 index 2fee883c30..0000000000 --- a/backend/api/src/get-phone-number.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { APIError, APIHandler } from 'api/helpers/endpoint' -import { createSupabaseDirectClient } from 'shared/supabase/init' - -export const getPhoneNumber: APIHandler<'phone-number'> = async ( - props, - auth -) => { - const pg = createSupabaseDirectClient() - - const number = await pg.oneOrNone( - `select phone_number from private_user_phone_numbers where user_id = $1`, - [auth.uid], - (r) => r?.phone_number as string - ) - if (!number) { - throw new APIError(400, 'User does not have a phone number') - } - - return { number } -} diff --git a/backend/api/src/gidx/callback.ts b/backend/api/src/gidx/callback.ts new file mode 100644 index 0000000000..3e98d8615d --- /dev/null +++ b/backend/api/src/gidx/callback.ts @@ -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', +} diff --git a/backend/api/src/gidx/get-verification-session.ts b/backend/api/src/gidx/get-verification-session.ts new file mode 100644 index 0000000000..677c400368 --- /dev/null +++ b/backend/api/src/gidx/get-verification-session.ts @@ -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 +} diff --git a/backend/api/src/gidx/get-verification-status.ts b/backend/api/src/gidx/get-verification-status.ts new file mode 100644 index 0000000000..b0fbc0bfb2 --- /dev/null +++ b/backend/api/src/gidx/get-verification-status.ts @@ -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', + } +} diff --git a/backend/api/src/gidx/register.ts b/backend/api/src/gidx/register.ts new file mode 100644 index 0000000000..19a9ee760b --- /dev/null +++ b/backend/api/src/gidx/register.ts @@ -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 +} diff --git a/backend/api/src/gidx/upload-document.ts b/backend/api/src/gidx/upload-document.ts new file mode 100644 index 0000000000..06e66313b5 --- /dev/null +++ b/backend/api/src/gidx/upload-document.ts @@ -0,0 +1,88 @@ +import { APIError, APIHandler } from 'api/helpers/endpoint' +import { getGIDXStandardParams } from 'shared/gidx/standard-params' +import { isProd, log } from 'shared/utils' +import * as admin from 'firebase-admin' +import { PROD_CONFIG } from 'common/envs/prod' +import { DEV_CONFIG } from 'common/envs/dev' +import { updateUser } from 'shared/supabase/users' +import { createSupabaseDirectClient } from 'shared/supabase/init' +import { + DocumentRegistrationResponse, + GIDX_REGISTATION_ENABLED, +} from 'common/gidx/gidx' + +const ENDPOINT = + 'https://api.gidx-service.in/v3.0/api/DocumentLibrary/DocumentRegistration' +export const uploadDocument: APIHandler<'upload-document-gidx'> = async ( + props, + auth +) => { + if (!GIDX_REGISTATION_ENABLED) + throw new APIError(400, 'GIDX registration is disabled') + const { fileUrl, CategoryType, fileName } = props + + const form = new FormData() + const fileBlob = await getBlobFromUrl(fileUrl) + form.append('file', fileBlob, fileName) + + const body = { + ...getGIDXStandardParams(), + MerchantCustomerID: auth.uid, + CategoryType, + DocumentStatus: 1, + } + form.append('json', JSON.stringify(body)) + const res = await fetch(ENDPOINT, { + method: 'POST', + body: form, + }) + if (!res.ok) { + throw new APIError(400, 'GIDX registration failed') + } + const data = (await res.json()) as DocumentRegistrationResponse + if (data.ResponseCode !== 0) { + throw new APIError(400, data.ResponseMessage) + } + const { Document } = data + log( + 'Uploaded document to GIDX successfully', + 'userId', + auth.uid, + 'DocumentID', + Document.DocumentID, + 'FileName', + Document.FileName + ) + await deleteFileFromFirebase(fileUrl) + const pg = createSupabaseDirectClient() + await updateUser(pg, auth.uid, { kycStatus: 'pending' }) + return { status: 'success' } +} + +const getBlobFromUrl = async (fileUrl: string): Promise => { + const response = await fetch(fileUrl) + if (!response.ok) { + throw new APIError(400, `Error retrieving file: ${response.status}`) + } + return await response.blob() +} + +const deleteFileFromFirebase = async (fileUrl: string) => { + const bucket = admin + .storage() + .bucket( + isProd() + ? PROD_CONFIG.firebaseConfig.privateBucket + : DEV_CONFIG.firebaseConfig.privateBucket + ) + const filePath = decodeURIComponent(fileUrl.split('/o/')[1].split('?')[0]) + const file = bucket.file(filePath) + + try { + await file.delete() + log(`Successfully deleted file: ${filePath}`) + } catch (error) { + log.error('Error deleting the file:', { error }) + throw new APIError(500, 'Error deleting identity file.') + } +} diff --git a/backend/api/src/helpers/endpoint.ts b/backend/api/src/helpers/endpoint.ts index 23f266ba11..0258291c7e 100644 --- a/backend/api/src/helpers/endpoint.ts +++ b/backend/api/src/helpers/endpoint.ts @@ -12,8 +12,10 @@ import { APISchema, ValidatedAPIParams, } from 'common/api/schema' +import { log } from 'shared/utils' import { getPrivateUserByKey } from 'shared/utils' + export type Json = Record | Json[] export type JsonHandler = ( req: Request, @@ -99,6 +101,9 @@ export const validate = (schema: T, val: unknown) => { error: i.message, } }) + if (issues.length > 0) { + log.error(issues.map((i) => `${i.field}: ${i.error}`).join('\n')) + } throw new APIError(400, 'Error validating request.', issues) } else { return result.data as z.infer diff --git a/backend/api/src/verify-phone-number.ts b/backend/api/src/verify-phone-number.ts index 1f9b4c172c..0e6b15eb1b 100644 --- a/backend/api/src/verify-phone-number.ts +++ b/backend/api/src/verify-phone-number.ts @@ -1,6 +1,6 @@ import { APIError, APIHandler } from 'api/helpers/endpoint' import { createSupabaseDirectClient } from 'shared/supabase/init' -import { getPrivateUser, getUser, isProd, log } from 'shared/utils' +import { getPrivateUser, getUser, log } from 'shared/utils' import { PHONE_VERIFICATION_BONUS, SUS_STARTING_BALANCE } from 'common/economy' import { SignupBonusTxn } from 'common/txn' import { runTxnFromBank } from 'shared/txn/run-txn' @@ -24,7 +24,7 @@ export const verifyPhoneNumber: APIHandler<'verify-phone-number'> = `, [auth.uid, phoneNumber] ) - if (userHasPhoneNumber && isProd()) { + if (userHasPhoneNumber) { throw new APIError(400, 'User verified phone number already.') } const authToken = process.env.TWILIO_AUTH_TOKEN @@ -44,7 +44,7 @@ export const verifyPhoneNumber: APIHandler<'verify-phone-number'> = ) .catch((e) => { log(e) - if (isProd()) throw new APIError(400, 'Phone number already exists') + throw new APIError(400, 'Phone number already exists') }) .then(() => { log(verification.status, { phoneNumber, otpCode }) @@ -77,7 +77,7 @@ export const verifyPhoneNumber: APIHandler<'verify-phone-number'> = if (!user) throw new APIError(401, `User ${auth.uid} not found`) const { verifiedPhone } = user - if (verifiedPhone === false) { + if (!verifiedPhone) { await updateUser(tx, auth.uid, { verifiedPhone: true, }) diff --git a/backend/shared/src/gidx/standard-params.ts b/backend/shared/src/gidx/standard-params.ts new file mode 100644 index 0000000000..30282efee5 --- /dev/null +++ b/backend/shared/src/gidx/standard-params.ts @@ -0,0 +1,11 @@ +import * as crypto from 'crypto' + +export const getGIDXStandardParams = () => ({ + // TODO: before merging into main, switch from sandbox key to production key in prod + ApiKey: process.env.GIDX_API_KEY, + MerchantID: process.env.GIDX_MERCHANT_ID, + ProductTypeID: process.env.GIDX_PRODUCT_TYPE_ID, + DeviceTypeID: process.env.GIDX_DEVICE_TYPE_ID, + ActivityTypeID: process.env.GIDX_ACTIVITY_TYPE_ID, + MerchantSessionID: crypto.randomUUID(), +}) diff --git a/backend/shared/src/helpers/get-phone-number.ts b/backend/shared/src/helpers/get-phone-number.ts new file mode 100644 index 0000000000..1f46314ae4 --- /dev/null +++ b/backend/shared/src/helpers/get-phone-number.ts @@ -0,0 +1,11 @@ +import { createSupabaseDirectClient } from 'shared/supabase/init' + +export const getPhoneNumber = async (userId: string) => { + const pg = createSupabaseDirectClient() + + return await pg.oneOrNone( + `select phone_number from private_user_phone_numbers where user_id = $1`, + [userId], + (r) => r?.phone_number as string + ) +} diff --git a/backend/shared/src/utils.ts b/backend/shared/src/utils.ts index 60cef10974..9bf7d4eebd 100644 --- a/backend/shared/src/utils.ts +++ b/backend/shared/src/utils.ts @@ -169,6 +169,15 @@ export const getPrivateUser = async ( convertPrivateUser ) } +export const getPrivateUserSupabase = (userId: string) => { + const pg = createSupabaseDirectClient() + + return pg.oneOrNone( + `select data from private_users where id = $1`, + [userId], + (row) => row.data as PrivateUser + ) +} export const getPrivateUserByKey = async ( apiKey: string, diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index 0ba3350aec..5d980f8d44 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -45,8 +45,13 @@ import { PortfolioMetrics, } from 'common/portfolio-metrics' import { ModReport } from '../mod-report' + +import { RegistrationReturnType } from 'common/reason-codes' +import { GIDXVerificationResponse, verificationParams } from 'common/gidx/gidx' + import { notification_preference } from 'common/user-notification-preferences' + // mqp: very unscientific, just balancing our willingness to accept load // with user willingness to put up with stale data export const DEFAULT_CACHE_STRATEGY = @@ -197,13 +202,6 @@ export const API = (_apiTypeCheck = { }) .strict(), }, - 'phone-number': { - method: 'GET', - visibility: 'undocumented', - authed: true, - returns: {} as { number: string }, - props: z.object({}).strict(), - }, 'bet/cancel/:betId': { method: 'POST', visibility: 'public', @@ -1288,6 +1286,45 @@ export const API = (_apiTypeCheck = { }) .strict(), }, + 'register-gidx': { + method: 'POST', + visibility: 'undocumented', + authed: true, + props: verificationParams, + returns: {} as RegistrationReturnType, + }, + 'get-verification-session-gidx': { + method: 'POST', + visibility: 'undocumented', + authed: true, + returns: {} as GIDXVerificationResponse, + props: verificationParams, + }, + 'get-verification-status-gidx': { + method: 'POST', + visibility: 'undocumented', + authed: true, + returns: {} as { status: string }, + props: z.object({}), + }, + 'upload-document-gidx': { + method: 'POST', + visibility: 'undocumented', + authed: true, + returns: {} as { status: string }, + props: z.object({ + CategoryType: z.number().gte(1).lte(7), + fileName: z.string(), + fileUrl: z.string(), + }), + }, + 'callback-gidx': { + method: 'POST', + visibility: 'undocumented', + authed: false, + returns: {} as any, + props: z.object({}) as any, + }, } as const) export type APIPath = keyof typeof API diff --git a/common/src/economy.ts b/common/src/economy.ts index 9209c32e0f..3b3c9c76ab 100644 --- a/common/src/economy.ts +++ b/common/src/economy.ts @@ -60,6 +60,7 @@ export const getTieredCost = ( export const STARTING_BALANCE = 100 export const PHONE_VERIFICATION_BONUS = 1000 +export const KYC_VERIFICATION_BONUS = 1000 export const NEXT_DAY_BONUS = 100 // Paid on day following signup export const MARKET_VISIT_BONUS = 100 // Paid on first distinct 5 market visits diff --git a/common/src/envs/dev.ts b/common/src/envs/dev.ts index 18191cbe80..b0a231ed5e 100644 --- a/common/src/envs/dev.ts +++ b/common/src/envs/dev.ts @@ -11,6 +11,7 @@ export const DEV_CONFIG: EnvConfig = { projectId: 'dev-mantic-markets', region: 'us-central1', storageBucket: 'dev-mantic-markets.appspot.com', + privateBucket: 'dev-mantic-markets-private', messagingSenderId: '134303100058', appId: '1:134303100058:web:27f9ea8b83347251f80323', measurementId: 'G-YJC9E37P37', diff --git a/common/src/envs/prod.ts b/common/src/envs/prod.ts index ca9541bfd7..3ac15b40d2 100644 --- a/common/src/envs/prod.ts +++ b/common/src/envs/prod.ts @@ -39,6 +39,7 @@ type FirebaseConfig = { projectId: string region?: string storageBucket: string + privateBucket: string messagingSenderId: string appId: string measurementId: string @@ -59,6 +60,7 @@ export const PROD_CONFIG: EnvConfig = { projectId: 'mantic-markets', region: 'us-central1', storageBucket: 'mantic-markets.appspot.com', + privateBucket: 'mantic-markets-private', messagingSenderId: '128925704902', appId: '1:128925704902:web:f61f86944d8ffa2a642dc7', measurementId: 'G-SSFK1Q138D', diff --git a/common/src/gidx/gidx.ts b/common/src/gidx/gidx.ts new file mode 100644 index 0000000000..607c557008 --- /dev/null +++ b/common/src/gidx/gidx.ts @@ -0,0 +1,67 @@ +import { z } from 'zod' + +export const GIDX_REGISTATION_ENABLED = false + +export const verificationParams = z.object({ + FirstName: z.string(), + LastName: z.string(), + DeviceGPS: z.object({ + Latitude: z.number(), + Longitude: z.number(), + Radius: z.number(), + Altitude: z.number(), + Speed: z.number(), + DateTime: z.string(), + }), + DateOfBirth: z.string(), + CitizenshipCountryCode: z.string(), + // Must supply address or ID info + AddressLine1: z.string().optional(), + AddressLine2: z.string().optional(), + City: z.string().optional(), + StateCode: z.string().optional(), + PostalCode: z.string().optional(), + CountryCode: z.string().optional(), + IdentificationTypeCode: z.number().gte(1).lte(4).optional(), + IdentificationNumber: z.string().optional(), + // TODO: remove these in production + DeviceIpAddress: z.string(), + EmailAddress: z.string(), + MerchantCustomerID: z.string(), +}) + +export type GIDXVerificationResponse = { + ReasonCodes: string[] + SessionID: string + SessionURL: string + SessionExpirationTime: string +} + +export type DocumentRegistrationResponse = { + ResponseCode: number + ResponseMessage: string + MerchantCustomerID: string + Document: GIDXDocument +} +export type GIDXDocument = { + DocumentID: string + CategoryType: number + DocumentStatus: number + FileName: string + FileSize: number + DateTime: string + DocumentNotes: { + AuthorName: string + NoteText: string + DateTime: string + }[] +} + +export type GPSData = { + Radius: number + Altitude: number + Latitude: number + Longitude: number + Speed: number + DateTime: string +} diff --git a/common/src/native-message.ts b/common/src/native-message.ts index 14fda18664..26cf3d2deb 100644 --- a/common/src/native-message.ts +++ b/common/src/native-message.ts @@ -1,12 +1,15 @@ +import { GPSData } from 'common/gidx/gidx' +import { Notification } from 'common/notification' + export type nativeToWebMessageType = | 'iapReceipt' | 'iapError' - | 'setIsNative' | 'nativeFbUser' | 'pushNotificationPermissionStatus' | 'pushToken' | 'notification' | 'link' + | 'location' export type nativeToWebMessage = { type: nativeToWebMessageType @@ -33,6 +36,24 @@ export type webToNativeMessageType = | 'theme' | 'log' | 'startedListening' + | 'locationRequested' export const IS_NATIVE_KEY = 'is-native' export const PLATFORM_KEY = 'native-platform' + +export type MesageTypeMap = { + location: GPSData | { error: string } + iapReceipt: { receipt: string } + iapError: object + nativeFbUser: object + pushNotificationPermissionStatus: { + status: 'denied' | 'undetermined' + } + pushToken: { + token: string + } + notification: Notification + link: { + url: string + } +} diff --git a/common/src/reason-codes.ts b/common/src/reason-codes.ts new file mode 100644 index 0000000000..97d8d18d20 --- /dev/null +++ b/common/src/reason-codes.ts @@ -0,0 +1,81 @@ +import { intersection } from 'lodash' + +export const timeoutCodes = [ + 'LL-TO', // Location timeout + 'DFP-TO', // Device Fingerprint Timeout +] + +export const identityErrorCodes = [ + 'ID-TO', // Identity Timeout + 'ID-INC', // Identity Incomplete + 'ID-FAIL', // Identity Verification Failure + 'ID-UNKN', // Identity Unknown +] + +// ID-VERIFIED supersedes all other identity error codes +export const hasIdentityError = (reasonCodes: string[]) => + intersection(identityErrorCodes, reasonCodes).length > 0 && + !reasonCodes.includes('ID-VERIFIED') + +export const otherErrorCodes: string[] = [ + ...timeoutCodes, + 'LL-FAIL', // Location service failed due to errors +] + +export const locationTemporarilyBlockedCodes = [ + 'DFP-HR-CONN', // Device Fingerprint High Risk Connection + 'LL-BLOCK', // location blocked +] + +export const locationBlockedCodes = [ + 'LL-HR', // high risk + 'LL-HR-CO', // high risk country + 'LL-WL', // location on watchlist +] + +export const underageErrorCodes = [ + 'ID-UA18', // Identity Under 18 + 'ID-UA19', // Identity Under 19 +] + +export const blockedCodes: string[] = [ + ...locationBlockedCodes, + + // Identity + ...underageErrorCodes, + 'ID-WL', // Identity on watchlist + 'ID-HR', // Identity High Risk + 'ID-BLOCK', // Identity Blocked + 'ID-EX', // Identity Exists already + 'ID-HVEL-ACTV', // Identity High Velocity Activity + 'ID-DECEASED', // Identity Deceased + + // Device + 'DFP-WL', // Device Fingerprint on watchlist + 'DFP-HR', // Device Fingerprint High Risk + 'DFP-HVEL-MIP-WEBREG', // Device Fingerprint High Velocity Matching IP ID Registration + 'DFP-IPNM', // Device Fingerprint IP Not Matching +] + +export const allowedFlaggedCodes: string[] = [ + 'ID-AGE-UNKN', // Identity Age Unknown, typically year is correct + 'ID-ADDR-UPA', // Identity Address Unknown + 'DFP-VPRP', // Device Fingerprint VPN, Proxy, or Relay Provider + 'DFP-VPRP-ANON', // Device Fingerprint Anon proxy + 'DFP-VPRP-CORP', // Device Fingerprint Corporate proxy + 'LL-ALERT-DIST', // Large distance between id location attempts +] + +export const allowedCodes: string[] = [ + 'ID-VERIFIED', // Identity Verified + 'ID-PASS', // Identity Verification Passed + 'ID-UA21', // Identity Under 21 + 'LL-OUT-US', // Location Outside US +] + +export type RegistrationReturnType = { + status: string + ReasonCodes: string[] + FraudConfidenceScore?: number + IdentityConfidenceScore?: number +} diff --git a/common/src/secrets.ts b/common/src/secrets.ts index a5c7ecbcb9..4d36984e06 100644 --- a/common/src/secrets.ts +++ b/common/src/secrets.ts @@ -28,6 +28,11 @@ export const secrets = ( 'TWILIO_AUTH_TOKEN', 'TWILIO_SID', 'TWILIO_VERIFY_SID', + 'GIDX_API_KEY', + 'GIDX_MERCHANT_ID', + 'GIDX_PRODUCT_TYPE_ID', + 'GIDX_DEVICE_TYPE_ID', + 'GIDX_ACTIVITY_TYPE_ID', // Some typescript voodoo to keep the string literal types while being not readonly. ] as const ).concat() diff --git a/common/src/user.ts b/common/src/user.ts index 7795538a4b..eb99c742ca 100644 --- a/common/src/user.ts +++ b/common/src/user.ts @@ -66,6 +66,9 @@ export type User = { isAdvancedTrader?: boolean verifiedPhone?: boolean purchasedMana?: boolean + kycMatch?: boolean + kycFlags?: string[] + kycStatus?: 'verified' | 'failed' | 'blocked' | 'pending' } export type PrivateUser = { diff --git a/native/App.tsx b/native/App.tsx index 75d04485d1..d03a9e2c8a 100644 --- a/native/App.tsx +++ b/native/App.tsx @@ -10,9 +10,7 @@ import { NativeEventEmitter, StyleSheet, SafeAreaView, - StatusBar as RNStatusBar, Dimensions, - View, Share, } from 'react-native' import Clipboard from '@react-native-clipboard/clipboard' @@ -28,16 +26,12 @@ import { IosIapListener } from 'components/ios-iap-listener' import { withIAPContext } from 'react-native-iap' import { getSourceUrl, Notification } from 'common/notification' import { + MesageTypeMap, nativeToWebMessage, nativeToWebMessageType, webToNativeMessage, } from 'common/native-message' -import { - handleWebviewKilled, - sharedWebViewProps, - handleWebviewError, - handleRenderError, -} from 'components/web-view-utils' +import { CustomWebview } from 'components/custom-webview' import { ExportLogsButton, log } from 'components/logger' import { ReadexPro_400Regular, useFonts } from '@expo-google-fonts/readex-pro' import Constants from 'expo-constants' @@ -45,13 +39,14 @@ import { NativeShareData } from 'common/native-share-data' import { clearData, getData, storeData } from 'lib/auth' import { SplashAuth } from 'components/splash-auth' import { useIsConnected } from 'lib/use-is-connected' +import { getLocation } from 'lib/location' + +// NOTE: you must change NEXT_PUBLIC_API_URL in dev.sh to match your local IP address. ie: +// "cross-env NEXT_PUBLIC_API_URL=172.20.10.2:8088 \ +// const baseUri = 'http://192.168.1.229:3000/gidx/register' -// NOTE: URIs other than manifold.markets and localhost:3000 won't work for API requests due to CORS -// this means no supabase jwt, placing bets, creating markets, etc. -// const baseUri = 'http://192.168.1.154:3000/' const baseUri = ENV === 'DEV' ? 'https://dev.manifold.markets/' : 'https://manifold.markets/' -const nativeQuery = `?nativePlatform=${Platform.OS}` const isIOS = Platform.OS === 'ios' const App = () => { // Init @@ -78,13 +73,7 @@ const App = () => { log('Got user from storage:', user.email) setFbUser(user) sendWebviewAuthInfo(user) - setFirebaseUserViaJson(user, app) - .catch((e) => { - log('Error setting user:', e) - }) - .then(() => { - log('User set successfully') - }) + await setFirebaseUserViaJson(user, app) } useEffect(() => { @@ -95,19 +84,23 @@ const App = () => { const sendWebviewAuthInfo = (user: FirebaseUser) => { // We use a timeout because sometimes the auth persistence manager is still undefined on the client side // Seems my iPhone 12 mini can regularly handle a shorter timeout - setTimeout(() => { - communicateWithWebview('nativeFbUser', user) - }, 100) - // My older android phone needs a bit longer - setTimeout(() => { - communicateWithWebview('nativeFbUser', user) - }, 500) + const timeouts = [100, 500, 1000, 3000] + timeouts.forEach((timeout) => { + setTimeout(() => { + communicateWithWebview('nativeFbUser', user) + }, timeout) + }) } // Url management - const [urlToLoad, setUrlToLoad] = useState( - baseUri + 'home' + nativeQuery - ) + const [urlToLoad, setUrlToLoad] = useState(() => { + const url = new URL(baseUri) + // url.pathname = 'home' + const params = new URLSearchParams() + params.set('nativePlatform', Platform.OS) + url.search = params.toString() + return url.toString() + }) const linkedUrl = Linking.useURL() const eventEmitter = new NativeEventEmitter( isIOS ? LinkingManager.default : null @@ -118,17 +111,20 @@ const App = () => { const [theme, setTheme] = useState<'dark' | 'light'>('light') const setEndpointWithNativeQuery = (endpoint?: string) => { - const newUrl = - baseUri + - (endpoint ?? 'home') + - nativeQuery + - `&rand=${Math.random().toString()}` - log('Setting new url:', newUrl) - setUrlToLoad(newUrl) + const url = new URL(baseUri) + url.pathname = endpoint ?? 'home' + setUrlWithNativeQuery(url.toString()) } - const setUrlWithNativeQuery = (url: String) => { - const newUrl = url + nativeQuery + `&rand=${Math.random().toString()}` + const setUrlWithNativeQuery = (urlString: string) => { + const url = new URL(urlString) + + const params = new URLSearchParams() + params.set('nativePlatform', Platform.OS) + params.set('rand', Math.random().toString()) + url.search = params.toString() + + const newUrl = url.toString() log('Setting new url:', newUrl) setUrlToLoad(newUrl) } @@ -153,7 +149,7 @@ const App = () => { if (hasLoadedWebView && listeningToNative.current) { communicateWithWebview( 'notification', - response.notification.request.content.data + response.notification.request.content.data as Notification ) setLastLinkInMemory(getSourceUrl(notification)) } else setEndpointWithNativeQuery(getSourceUrl(notification)) @@ -290,7 +286,6 @@ const App = () => { if (finalStatus !== 'granted') { communicateWithWebview('pushNotificationPermissionStatus', { status: finalStatus, - userId: fbUser?.uid, }) return null } @@ -318,12 +313,10 @@ const App = () => { if (token) communicateWithWebview('pushToken', { token, - userId: fbUser?.uid, }) } else communicateWithWebview('pushNotificationPermissionStatus', { status, - userId: fbUser?.uid, }) }) } else if (type === 'copyToClipboard') { @@ -335,7 +328,6 @@ const App = () => { if (token) communicateWithWebview('pushToken', { token, - userId: fbUser?.uid, }) }) } else if (type === 'signOut') { @@ -374,6 +366,10 @@ const App = () => { log('Client started listening') listeningToNative.current = true if (fbUser) sendWebviewAuthInfo(fbUser) + } else if (type === 'locationRequested') { + log('Location requested from web') + const location = await getLocation() + communicateWithWebview('location', location) } else { log('Unhandled message from web type: ', type) log('Unhandled message from web data: ', data) @@ -392,9 +388,9 @@ const App = () => { }) } - const communicateWithWebview = ( - type: nativeToWebMessageType, - data: object + const communicateWithWebview = ( + type: T, + data: MesageTypeMap[T] ) => { log( 'Sending message to webview:', @@ -430,12 +426,6 @@ const App = () => { overflow: 'hidden', backgroundColor: backgroundColor, }, - webView: { - display: fullyLoaded ? 'flex' : 'none', - overflow: 'hidden', - marginTop: isIOS ? 0 : RNStatusBar.currentHeight ?? 0, - marginBottom: 0, - }, }) const handleExternalLink = (url: string) => { @@ -474,34 +464,16 @@ const App = () => { style={theme === 'dark' ? 'light' : 'dark'} hidden={false} /> - - - { - log('WebView onLoadEnd for url:', urlToLoad) - setHasLoadedWebView(true) - }} - source={{ uri: urlToLoad }} - ref={webview} - onError={(e) => handleWebviewError(e, resetWebView)} - renderError={(e) => handleRenderError(e, width, height)} - onOpenWindow={(e) => handleExternalLink(e.nativeEvent.targetUrl)} - onRenderProcessGone={(e) => handleWebviewKilled(e, resetWebView)} - onContentProcessDidTerminate={(e) => - handleWebviewKilled(e, resetWebView) - } - onMessage={async (m) => { - try { - await handleMessageFromWebview(m) - } catch (e) { - log('Error in handleMessageFromWebview', e) - } - }} - /> - + {/**/} diff --git a/native/README.md b/native/README.md index d757e79f76..b4c258ccf6 100644 --- a/native/README.md +++ b/native/README.md @@ -18,11 +18,17 @@ We're using Expo to help with android and ios builds. You can find more informat 3. **Android**: - `yarn android:dev` or `yarn android:prod` builds and installs the dev client on your device automatically - Scan the QR code with the app (it opens automatically after installing) +4. **Locally hosted manifold**: + - Set the `NEXT_PUBLIC_API_URL` in dev.sh to your local ip address + - Run `dev.sh prod` or `dev.sh dev` to start the local server + - Change the `baseUri` in `App.tsx` to your local ip address + - Follow one of the Android or iOS steps to start the app on your device + - **Note:** when switching between dev and prod you'll have to run `yarn clear` & Ctrl+C to clear the env variable. - Want to see console logs? (Only works on android): - - `$ ngrok http 3000` in a separate terminal - - Change the `baseUri` in `App.tsx` to the ngrok url + - Set the `NEXT_PUBLIC_API_URL` in dev.sh to your local ip address + - Change the `baseUri` in `App.tsx` to your local ip address - `$ yarn android:prod` to start the app on your device - On your computer, navigate to `chrome://inspect/#devices` in chrome and click inspect on the app - Want to see app logs of a production build? (Only works on android): diff --git a/native/android/.gitignore b/native/android/.gitignore index 877b87e9a5..8a6be07718 100644 --- a/native/android/.gitignore +++ b/native/android/.gitignore @@ -10,6 +10,7 @@ build/ local.properties *.iml *.hprof +.cxx/ # Bundle artifacts *.jsbundle diff --git a/native/android/app/build.gradle b/native/android/app/build.gradle index fde3e51326..7e85d5af20 100644 --- a/native/android/app/build.gradle +++ b/native/android/app/build.gradle @@ -18,15 +18,14 @@ react { // works correctly with Expo projects. cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim()) bundleCommand = "export:embed" + /* Folders */ // The root of your project, i.e. where "package.json" lives. Default is '..' // root = file("../") // The folder where the react-native NPM package is. Default is ../node_modules/react-native // reactNativeDir = file("../node_modules/react-native") - // The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen - // codegenDir = file("../node_modules/react-native-codegen") - // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js - // cliFile = file("../node_modules/react-native/cli.js") + // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen + // codegenDir = file("../node_modules/@react-native/codegen") /* Variants */ // The list of variants to that are debuggable. For those we're going to @@ -37,6 +36,7 @@ react { /* Bundling */ // A list containing the node command and its flags. Default is just 'node'. // nodeExecutableAndArgs = ["node"] + // // The path to the CLI configuration file. Default is empty. // bundleConfig = file(../rn-cli.config.js) @@ -77,7 +77,6 @@ def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInRelea */ def jscFlavor = 'org.webkit:android-jsc:+' - android { ndkVersion rootProject.ext.ndkVersion @@ -93,8 +92,6 @@ android { versionName "2.0.46" missingDimensionStrategy 'store', 'play' } - - signingConfigs { debug { storeFile file('debug.keystore') @@ -116,13 +113,11 @@ android { proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } - packagingOptions { - jniLibs { - useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false) - } + jniLibs { + useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false) + } } - } // Apply static values from `gradle.properties` to the `android.packagingOptions` @@ -153,32 +148,28 @@ dependencies { def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true"; def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true"; - if (isGifEnabled) { // For animated gif support - implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}") + implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}") } if (isWebpEnabled) { // For webp support - implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}") + implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}") if (isWebpAnimatedEnabled) { // Animated webp support - implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}") + implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}") } } - implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") - - if (hermesEnabled.toBoolean()) { implementation("com.facebook.react:hermes-android") } else { implementation jscFlavor } } -apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); +apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); applyNativeModulesAppBuildGradle(project) apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/native/android/app/src/debug/AndroidManifest.xml b/native/android/app/src/debug/AndroidManifest.xml index 99e38fc5f8..3ec2507bab 100644 --- a/native/android/app/src/debug/AndroidManifest.xml +++ b/native/android/app/src/debug/AndroidManifest.xml @@ -3,5 +3,5 @@ - + diff --git a/native/android/app/src/main/AndroidManifest.xml b/native/android/app/src/main/AndroidManifest.xml index 0473bc4022..545bbb065e 100644 --- a/native/android/app/src/main/AndroidManifest.xml +++ b/native/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,6 @@ - + + + @@ -11,7 +13,7 @@ - + diff --git a/native/android/app/src/main/java/com/markets/manifold/MainActivity.kt b/native/android/app/src/main/java/com/markets/manifold/MainActivity.kt index bb73866e15..30525aa515 100644 --- a/native/android/app/src/main/java/com/markets/manifold/MainActivity.kt +++ b/native/android/app/src/main/java/com/markets/manifold/MainActivity.kt @@ -58,4 +58,4 @@ class MainActivity : ReactActivity() { // because it's doing more than [Activity.moveTaskToBack] in fact. super.invokeDefaultOnBackPressed() } -} \ No newline at end of file +} diff --git a/native/android/app/src/main/java/com/markets/manifold/MainApplication.kt b/native/android/app/src/main/java/com/markets/manifold/MainApplication.kt index b271ea0fca..e6982f8bd5 100644 --- a/native/android/app/src/main/java/com/markets/manifold/MainApplication.kt +++ b/native/android/app/src/main/java/com/markets/manifold/MainApplication.kt @@ -52,4 +52,4 @@ class MainApplication : Application(), ReactApplication { super.onConfigurationChanged(newConfig) ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) } -} \ No newline at end of file +} diff --git a/native/android/app/src/main/res/drawable/rn_edit_text_material.xml b/native/android/app/src/main/res/drawable/rn_edit_text_material.xml index f35d996202..5c25e728ea 100644 --- a/native/android/app/src/main/res/drawable/rn_edit_text_material.xml +++ b/native/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -17,10 +17,11 @@ android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material" android:insetRight="@dimen/abc_edit_text_inset_horizontal_material" android:insetTop="@dimen/abc_edit_text_inset_top_material" - android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"> + android:insetBottom="@dimen/abc_edit_text_inset_bottom_material" + > -