Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/configs/payments.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ TORUS_GOOGLE_VERIFIER="capsule-social-google-chris"
TORUS_GOOGLE_CLIENTID="653379121360-j8t9ua763vfvd86d1qjguonhrgqvkigo.apps.googleusercontent.com"
TORUS_DISCORD_VERIFIER="capsule-social-test-v2-discord"
TORUS_DISCORD_CLIENTID="906210984396468275"
PHONE_ENABLED=true
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@
"@tailwindcss/typography": "^0.4.1",
"@toruslabs/customauth": "^10.1.1",
"@toruslabs/openlogin-ed25519": "^2.0.0",
"@types/intl-tel-input": "^17.0.5",
"axios": "^0.27.2",
"browser-image-compression": "^2.0.0",
"dompurify": "^2.4.0",
"highlight.js": "^11.6.0",
"intl-tel-input": "^17.0.18",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"marked": "^4.0.19",
Expand Down
18 changes: 17 additions & 1 deletion src/backend/funder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import axios from 'axios'
import { checkAccountStatus, getIsAccountIdOnboarded } from './near'
import { capsuleServer, sufficientFunds } from './utilities/config'

export async function requestOTP(phoneNumber: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should also add a captchaRes to requestOTP() function. Basically, when phone number toggle is on, we want the user to solve captcha when request for an OTP from their phone number (ie. requestOTP() function)

const response = await axios.post(`${capsuleServer}/sendOtp`, {
phoneNumber,
})
return response.data.data
}

export async function getFundTransferStatus(accountId: string): Promise<`PROCESSING` | `SENT` | `FAILED`> {
const response = await axios.get(`${capsuleServer}/onboard/sponsor/status?accountId=${accountId}`)
return response.data.data
Expand Down Expand Up @@ -49,9 +56,18 @@ export function waitForFunds(accountId: string) {
}

export async function requestOnboard(captchaRes: string, accountId: string) {
const response = await axios.post(`${capsuleServer}/onboard`, {
const response = await axios.post(`${capsuleServer}/onboard/captcha`, {
captchaRes,
accountId,
})
return response.data.data
}

export async function requestPhoneOnboard(phoneNumber: string, code: string, accountId: string) {
const response = await axios.post(`${capsuleServer}/onboard/phone`, {
phoneNumber,
code,
accountId,
})
return response.data.data
}
2 changes: 2 additions & 0 deletions src/backend/utilities/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const sufficientFunds = process.env.SUFFICIENT_ACCOUNT_FUNDS || `81800000
export const sigValidity = 5 * 60000
export const bootstrapNodes = process.env.BOOTSTRAP_NODES ? JSON.parse(process.env.BOOTSTRAP_NODES) : defaultBootstraps

export const isPhoneEnabled: boolean = process.env.PHONE_ENABLED === `true`

export const stripePublishableKey =
process.env.STRIPE_PUBLISHABLE_KEY ||
`pk_test_51I81pBCPCJ3FaYLGnUrPUMxipudV7gWWA7qAiqIVMAqnULA4a2uluUgBQxX8yKzAe2iGYOoSMX2rSbF45wtKlhXI00Olk8hJmc`
Expand Down
19 changes: 15 additions & 4 deletions src/components/register/SelectID.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,21 @@
</template>

<script lang="ts">
import Vue from 'vue'
import Vue, { PropType } from 'vue'
import { mapMutations } from 'vuex'

import { MutationType, namespace as sessionStoreNamespace } from '~/store/session'
import { ValidationError } from '@/errors'
import { requestOnboard, waitForFunds } from '@/backend/funder'
import { requestOnboard, waitForFunds, hasSufficientFunds } from '@/backend/funder'
import { validateUsernameNEAR } from '@/backend/near'
import { hcaptchaSiteKey } from '@/backend/utilities/config'
import ChevronLeft from '@/components/icons/ChevronLeft.vue'

import { IWalletStatus } from '@/backend/auth'
interface IData {
id: string
siteKey: string
loadingState: `checking_id` | `hcaptcha_loading` | `smart_contract` | `transfer_funds` | null
captchaID: string | null
isLoading: boolean
}

export default Vue.extend({
Expand All @@ -70,13 +70,18 @@ export default Vue.extend({
type: String,
required: true,
},
userInfo: {
type: Object as PropType<IWalletStatus>,
required: true,
},
},
data(): IData {
return {
id: ``,
siteKey: hcaptchaSiteKey,
loadingState: null,
captchaID: null,
isLoading: false,
}
},
async mounted() {
Expand Down Expand Up @@ -110,6 +115,7 @@ export default Vue.extend({
location.reload()
},
async handleRegisterID() {
this.isLoading = true
try {
if (!this.captchaID) {
return
Expand All @@ -119,6 +125,7 @@ export default Vue.extend({
const idValidity = await validateUsernameNEAR(this.id)
if (idValidity.error) {
this.loadingState = null
this.isLoading = false
throw new ValidationError(idValidity.error)
}
this.loadingState = `hcaptcha_loading`
Expand Down Expand Up @@ -150,8 +157,12 @@ export default Vue.extend({
this.$handleError(error)
} finally {
this.loadingState = null
this.isLoading = false
}
},
hasEnoughFunds(): boolean {
return hasSufficientFunds(this.accountId)
},
},
})
</script>
51 changes: 47 additions & 4 deletions src/components/register/SignUp.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
<template>
<div class="flex flex-row w-full items-center justify-center">
<article v-if="!downloadKey" class="flex flex-row w-full items-center justify-center">
<VerifyPhone
v-if="phoneEnabled && (!hasEnoughFunds() || !onboarded)"
:accountId="userInfo.accountId"
class="w-full h-full xl:w-1/2"
@updateFunds="updateFunds"
@setIsOnboarded="setIsOnboarded"
/>
<!-- Step 2: Choose ID -->
<SelectID :funds="funds" :accountId="userInfo.accountId" class="w-full h-full xl:w-1/2" @verify="verify" />
<SelectID v-else :funds="funds" :accountId="userInfo.accountId" class="w-full h-full xl:w-1/2" @verify="verify" />
</article>
<!-- Step 4: Download key -->
<DownloadKey
Expand All @@ -21,25 +28,35 @@ import { mapMutations } from 'vuex'

import SelectID from './SelectID.vue'
import DownloadKey from './DownloadKey.vue'
import VerifyPhone from './VerifyPhone.vue'

import { getUsernameNEAR, removeNearPrivateKey, walletLogout } from '@/backend/near'

import {
checkAccountStatus,
getIsAccountIdOnboarded,
getUsernameNEAR,
removeNearPrivateKey,
walletLogout,
} from '@/backend/near'
import { hasSufficientFunds } from '@/backend/funder'
import { MutationType, createSessionFromProfile, namespace as sessionStoreNamespace } from '~/store/session'
import { setNearUserFromPrivateKey, login, register, IAuthResult, IWalletStatus } from '@/backend/auth'
import { ValidationError } from '@/errors'
import { nearNetwork } from '@/backend/utilities/config'
import { nearNetwork, isPhoneEnabled } from '@/backend/utilities/config'

interface IData {
funds: string
username: null | string
isLoading: boolean
downloadKey: boolean
onboarded: boolean
phoneEnabled: boolean
}

export default Vue.extend({
components: {
DownloadKey,
SelectID,
VerifyPhone,
},
props: {
userInfo: {
Expand All @@ -53,13 +70,22 @@ export default Vue.extend({
username: null,
isLoading: true,
downloadKey: false,
onboarded: false,
phoneEnabled: isPhoneEnabled,
}
},
async created() {
this.$emit(`setIsLoading`, true)
try {
const username = await getUsernameNEAR(this.userInfo.accountId)
if (!username) {
if (this.phoneEnabled) {
const [, onboarded] = await Promise.all([
this.checkFunds(),
await getIsAccountIdOnboarded(this.userInfo.accountId),
])
this.onboarded = onboarded
}
this.$emit(`setIsLoading`, false)
return
}
Expand Down Expand Up @@ -99,6 +125,23 @@ export default Vue.extend({
changeBio: MutationType.CHANGE_BIO,
changeLocation: MutationType.CHANGE_LOCATION,
}),
hasEnoughFunds(): boolean {
return hasSufficientFunds(this.funds)
},
async checkFunds() {
const accountId = this.userInfo.accountId
if (!accountId) {
return
}
const status = await checkAccountStatus(accountId)
this.funds = status.balance
},
updateFunds(balance: string) {
this.funds = balance
},
setIsOnboarded(onboarded: boolean) {
this.onboarded = onboarded
},
async verify(id: string) {
if (!this.userInfo) {
throw new Error(`Unexpected condition!`)
Expand Down
145 changes: 145 additions & 0 deletions src/components/register/VerifyPhone.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<template>
<article>
<h1 class="text-lightPrimaryText dark:text-gray1 text-4xl font-bold">Sign up</h1>
<!-- Enter phone number -->
<div v-show="!otpSent">
<p class="text-gray7 dark:text-gray3 my-10 text-center">
Verify you’re a human with your phone number so that Blogchain can fund your wallet.
</p>
<label for="phoneNumber" class="text-gray5 dark:text-gray3 block pb-1 text-sm font-semibold">Phone Number</label>
<input
id="phoneNumber"
v-model="phoneNumber"
type="tel"
class="focus:outline-none focus:border-primary text-primary dark:text-darkPrimaryText bg-gray2 dark:bg-gray7 mt-1 mb-5 w-full rounded-lg px-3 py-2 font-sans text-sm"
/>
<div class="flex w-full justify-end mt-4">
<BrandedButton :text="otpSent ? `Re-send code` : `Send Code`" :action="sendOTP" />
</div>
<h6 v-show="isLoading" class="text-primary text-center">Sending SMS...</h6>
</div>
<!-- Enter SMS code to complete verify -->
<div v-show="otpSent" class="mt-10">
<label for="otp" class="text-gray5 dark:text-gray3 block pb-1 text-sm font-semibold"
>Enter the one-time verification code sent to your phone number.</label
>
<input
id="otp"
v-model="otp"
type="text"
placeholder=""
class="focus:outline-none focus:border-primary text-primary dark:text-darkPrimaryText bg-gray2 dark:bg-gray7 mt-1 mb-5 w-full rounded-lg px-3 py-2 font-sans text-sm"
/>
<BrandedButton v-show="!isLoading && !waitingForFunds" :text="`Verify`" class="w-full" :action="validateOTP" />
<h6 v-show="isLoading" class="text-primary text-center">Verifying...</h6>
<h6 v-show="waitingForFunds" class="text-primary text-center">Executing smart contract...</h6>
</div>
<p v-show="otpSent" class="text-gray7 dark:text-gray2 mt-10 text-center text-sm">
Didn't receive a code?
<button class="text-primary font-bold" @click="otpSent = false">
Check your phone number and request a new one
</button>
</p>
</article>
</template>

<script lang="ts">
import Vue from 'vue'
import intlTelInput from 'intl-tel-input'
import { AxiosError } from 'axios'
import BrandedButton from '@/components/BrandedButton.vue'
import { requestOTP, requestPhoneOnboard, waitForFunds } from '@/backend/funder'
interface IData {
otp: string
otpSent: boolean
iti: null | intlTelInput.Plugin
phoneNumber: string
inputCode: string
isLoading: boolean
waitingForFunds: boolean
}
export default Vue.extend({
components: {
BrandedButton,
},
props: {
accountId: {
type: String,
required: true,
},
},
data(): IData {
return {
otp: ``,
otpSent: false,
iti: null,
phoneNumber: ``,
inputCode: ``,
isLoading: false,
waitingForFunds: false,
}
},
mounted() {
const input = document.querySelector(`#phoneNumber`)
if (input) {
this.iti = intlTelInput(input, {
utilsScript: require(`intl-tel-input/build/js/utils`),
// any initialisation options go here
})
}
},
methods: {
async sendOTP() {
if (this.iti === null) {
return
}
this.phoneNumber = this.iti.getNumber()
if (!this.iti.isValidNumber()) {
this.$toastError(`Invalid phone number`)
return
}
this.isLoading = true
await requestOTP(this.phoneNumber)
this.otpSent = true
this.$toastSuccess(
`If you haven't used this phone number before on Blogchain, you'll receive a code on your phone`,
)
this.isLoading = false
},
async validateOTP() {
try {
if (this.otp.length !== 6) {
this.$toastError(`OTP should have 6 digits`)
return
}
if (!this.accountId) {
return
}
this.isLoading = true
await requestPhoneOnboard(this.phoneNumber, this.otp, this.accountId)
this.isLoading = false
this.waitingForFunds = true
const { balance } = await waitForFunds(this.accountId)
this.waitingForFunds = false
this.$emit(`setIsOnboarded`, true)
this.$emit(`updateFunds`, balance)
} catch (err: any) {
this.isLoading = false
this.waitingForFunds = false
if (err instanceof AxiosError && err.response) {
this.otp = ``
this.$toastError(err.response.data.error)
return
}
this.$toastError(err.message)
throw err
}
},
},
})
</script>
<style>
.iti {
width: 100%;
}
</style>
Loading