Skip to content

Commit 86f929c

Browse files
committed
feat: toggle phone number registeration
1 parent 0882251 commit 86f929c

File tree

8 files changed

+254
-38
lines changed

8 files changed

+254
-38
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,12 @@
6060
"@tailwindcss/typography": "^0.4.1",
6161
"@toruslabs/customauth": "^10.1.1",
6262
"@toruslabs/openlogin-ed25519": "^2.0.0",
63+
"@types/intl-tel-input": "^17.0.5",
6364
"axios": "^0.27.2",
6465
"browser-image-compression": "^2.0.0",
6566
"dompurify": "^2.4.0",
6667
"highlight.js": "^11.6.0",
68+
"intl-tel-input": "^17.0.18",
6769
"libsodium-wrappers": "^0.7.10",
6870
"lodash": "^4.17.21",
6971
"marked": "^4.0.19",

src/backend/funder.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import axios from 'axios'
22
import { checkAccountStatus, getIsAccountIdOnboarded } from './near'
33
import { capsuleServer, sufficientFunds } from './utilities/config'
44

5+
export async function requestOTP(phoneNumber: string) {
6+
const response = await axios.post(`${capsuleServer}/sendOtp`, {
7+
phoneNumber,
8+
})
9+
return response.data.data
10+
}
11+
512
export async function getFundTransferStatus(accountId: string): Promise<`PROCESSING` | `SENT` | `FAILED`> {
613
const response = await axios.get(`${capsuleServer}/onboard/sponsor/status?accountId=${accountId}`)
714
return response.data.data
@@ -55,3 +62,12 @@ export async function requestOnboard(captchaRes: string, accountId: string) {
5562
})
5663
return response.data.data
5764
}
65+
66+
export async function requestPhoneOnboard(phoneNumber: string, code: string, accountId: string) {
67+
const response = await axios.post(`${capsuleServer}/onboard`, {
68+
phoneNumber,
69+
code,
70+
accountId,
71+
})
72+
return response.data.data
73+
}

src/backend/utilities/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export const sufficientFunds = process.env.SUFFICIENT_ACCOUNT_FUNDS || `81800000
1919
export const sigValidity = 5 * 60000
2020
export const bootstrapNodes = process.env.BOOTSTRAP_NODES ? JSON.parse(process.env.BOOTSTRAP_NODES) : defaultBootstraps
2121

22+
export const isPhoneEnabled: boolean = process.env.PHONE_ENABLED === `true`
23+
2224
export const stripePublishableKey =
2325
process.env.STRIPE_PUBLISHABLE_KEY ||
2426
`pk_test_51I81pBCPCJ3FaYLGnUrPUMxipudV7gWWA7qAiqIVMAqnULA4a2uluUgBQxX8yKzAe2iGYOoSMX2rSbF45wtKlhXI00Olk8hJmc`

src/components/register/SelectID.vue

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,21 @@
4444
</template>
4545

4646
<script lang="ts">
47-
import Vue from 'vue'
47+
import Vue, { PropType } from 'vue'
4848
import { mapMutations } from 'vuex'
49-
5049
import { MutationType, namespace as sessionStoreNamespace } from '~/store/session'
5150
import { ValidationError } from '@/errors'
52-
import { requestOnboard, waitForFunds } from '@/backend/funder'
51+
import { requestOnboard, waitForFunds, hasSufficientFunds } from '@/backend/funder'
5352
import { validateUsernameNEAR } from '@/backend/near'
5453
import { hcaptchaSiteKey } from '@/backend/utilities/config'
5554
import ChevronLeft from '@/components/icons/ChevronLeft.vue'
56-
55+
import { IWalletStatus } from '@/backend/auth'
5756
interface IData {
5857
id: string
5958
siteKey: string
6059
loadingState: `checking_id` | `hcaptcha_loading` | `smart_contract` | `transfer_funds` | null
6160
captchaID: string | null
61+
isLoading: boolean
6262
}
6363
6464
export default Vue.extend({
@@ -70,13 +70,18 @@ export default Vue.extend({
7070
type: String,
7171
required: true,
7272
},
73+
userInfo: {
74+
type: Object as PropType<IWalletStatus>,
75+
required: true,
76+
},
7377
},
7478
data(): IData {
7579
return {
7680
id: ``,
7781
siteKey: hcaptchaSiteKey,
7882
loadingState: null,
7983
captchaID: null,
84+
isLoading: false,
8085
}
8186
},
8287
async mounted() {
@@ -110,6 +115,7 @@ export default Vue.extend({
110115
location.reload()
111116
},
112117
async handleRegisterID() {
118+
this.isLoading = true
113119
try {
114120
if (!this.captchaID) {
115121
return
@@ -119,6 +125,7 @@ export default Vue.extend({
119125
const idValidity = await validateUsernameNEAR(this.id)
120126
if (idValidity.error) {
121127
this.loadingState = null
128+
this.isLoading = false
122129
throw new ValidationError(idValidity.error)
123130
}
124131
this.loadingState = `hcaptcha_loading`
@@ -150,8 +157,12 @@ export default Vue.extend({
150157
this.$handleError(error)
151158
} finally {
152159
this.loadingState = null
160+
this.isLoading = false
153161
}
154162
},
163+
hasEnoughFunds(): boolean {
164+
return hasSufficientFunds(this.accountId)
165+
},
155166
},
156167
})
157168
</script>

src/components/register/SignUp.vue

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
<template>
22
<div class="flex flex-row w-full items-center justify-center">
33
<article v-if="!downloadKey" class="flex flex-row w-full items-center justify-center">
4+
<VerifyPhone
5+
v-if="phoneEnabled && (!hasEnoughFunds() || !onboarded)"
6+
:accountId="userInfo.accountId"
7+
class="w-full h-full xl:w-1/2"
8+
@updateFunds="updateFunds"
9+
@setIsOnboarded="setIsOnboarded"
10+
/>
411
<!-- Step 2: Choose ID -->
5-
<SelectID :funds="funds" :accountId="userInfo.accountId" class="w-full h-full xl:w-1/2" @verify="verify" />
12+
<SelectID v-else :funds="funds" :accountId="userInfo.accountId" class="w-full h-full xl:w-1/2" @verify="verify" />
613
</article>
714
<!-- Step 4: Download key -->
815
<DownloadKey
@@ -21,25 +28,35 @@ import { mapMutations } from 'vuex'
2128
2229
import SelectID from './SelectID.vue'
2330
import DownloadKey from './DownloadKey.vue'
31+
import VerifyPhone from './VerifyPhone.vue'
2432
25-
import { getUsernameNEAR, removeNearPrivateKey, walletLogout } from '@/backend/near'
26-
33+
import {
34+
checkAccountStatus,
35+
getIsAccountIdOnboarded,
36+
getUsernameNEAR,
37+
removeNearPrivateKey,
38+
walletLogout,
39+
} from '@/backend/near'
40+
import { hasSufficientFunds } from '@/backend/funder'
2741
import { MutationType, createSessionFromProfile, namespace as sessionStoreNamespace } from '~/store/session'
2842
import { setNearUserFromPrivateKey, login, register, IAuthResult, IWalletStatus } from '@/backend/auth'
2943
import { ValidationError } from '@/errors'
30-
import { nearNetwork } from '@/backend/utilities/config'
44+
import { nearNetwork, isPhoneEnabled } from '@/backend/utilities/config'
3145
3246
interface IData {
3347
funds: string
3448
username: null | string
3549
isLoading: boolean
3650
downloadKey: boolean
51+
onboarded: boolean
52+
phoneEnabled: boolean
3753
}
3854
3955
export default Vue.extend({
4056
components: {
4157
DownloadKey,
4258
SelectID,
59+
VerifyPhone,
4360
},
4461
props: {
4562
userInfo: {
@@ -53,13 +70,22 @@ export default Vue.extend({
5370
username: null,
5471
isLoading: true,
5572
downloadKey: false,
73+
onboarded: false,
74+
phoneEnabled: isPhoneEnabled,
5675
}
5776
},
5877
async created() {
5978
this.$emit(`setIsLoading`, true)
6079
try {
6180
const username = await getUsernameNEAR(this.userInfo.accountId)
6281
if (!username) {
82+
if (this.phoneEnabled) {
83+
const [, onboarded] = await Promise.all([
84+
this.checkFunds(),
85+
await getIsAccountIdOnboarded(this.userInfo.accountId),
86+
])
87+
this.onboarded = onboarded
88+
}
6389
this.$emit(`setIsLoading`, false)
6490
return
6591
}
@@ -99,6 +125,23 @@ export default Vue.extend({
99125
changeBio: MutationType.CHANGE_BIO,
100126
changeLocation: MutationType.CHANGE_LOCATION,
101127
}),
128+
hasEnoughFunds(): boolean {
129+
return hasSufficientFunds(this.funds)
130+
},
131+
async checkFunds() {
132+
const accountId = this.userInfo.accountId
133+
if (!accountId) {
134+
return
135+
}
136+
const status = await checkAccountStatus(accountId)
137+
this.funds = status.balance
138+
},
139+
updateFunds(balance: string) {
140+
this.funds = balance
141+
},
142+
setIsOnboarded(onboarded: boolean) {
143+
this.onboarded = onboarded
144+
},
102145
async verify(id: string) {
103146
if (!this.userInfo) {
104147
throw new Error(`Unexpected condition!`)
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<template>
2+
<article>
3+
<h1 class="text-lightPrimaryText dark:text-gray1 text-4xl font-bold">Sign up</h1>
4+
<!-- Enter phone number -->
5+
<div v-show="!otpSent">
6+
<p class="text-gray7 dark:text-gray3 my-10 text-center">
7+
Verify you’re a human with your phone number so that Blogchain can fund your wallet.
8+
</p>
9+
<label for="phoneNumber" class="text-gray5 dark:text-gray3 block pb-1 text-sm font-semibold">Phone Number</label>
10+
<input
11+
id="phoneNumber"
12+
v-model="phoneNumber"
13+
type="tel"
14+
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"
15+
/>
16+
<div class="flex w-full justify-end mt-4">
17+
<BrandedButton :text="otpSent ? `Re-send code` : `Send Code`" :action="sendOTP" />
18+
</div>
19+
<h6 v-show="isLoading" class="text-primary text-center">Sending SMS...</h6>
20+
</div>
21+
<!-- Enter SMS code to complete verify -->
22+
<div v-show="otpSent" class="mt-10">
23+
<label for="otp" class="text-gray5 dark:text-gray3 block pb-1 text-sm font-semibold"
24+
>Enter the one-time verification code sent to your phone number.</label
25+
>
26+
<input
27+
id="otp"
28+
v-model="otp"
29+
type="text"
30+
placeholder=""
31+
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"
32+
/>
33+
<BrandedButton v-show="!isLoading && !waitingForFunds" :text="`Verify`" class="w-full" :action="validateOTP" />
34+
<h6 v-show="isLoading" class="text-primary text-center">Verifying...</h6>
35+
<h6 v-show="waitingForFunds" class="text-primary text-center">Executing smart contract...</h6>
36+
</div>
37+
<p v-show="otpSent" class="text-gray7 dark:text-gray2 mt-10 text-center text-sm">
38+
Didn't receive a code?
39+
<button class="text-primary font-bold" @click="otpSent = false">
40+
Check your phone number and request a new one
41+
</button>
42+
</p>
43+
</article>
44+
</template>
45+
46+
<script lang="ts">
47+
import Vue from 'vue'
48+
import intlTelInput from 'intl-tel-input'
49+
import { AxiosError } from 'axios'
50+
import BrandedButton from '@/components/BrandedButton.vue'
51+
import { requestOTP, requestPhoneOnboard, waitForFunds } from '@/backend/funder'
52+
interface IData {
53+
otp: string
54+
otpSent: boolean
55+
iti: null | intlTelInput.Plugin
56+
phoneNumber: string
57+
inputCode: string
58+
isLoading: boolean
59+
waitingForFunds: boolean
60+
}
61+
export default Vue.extend({
62+
components: {
63+
BrandedButton,
64+
},
65+
props: {
66+
accountId: {
67+
type: String,
68+
required: true,
69+
},
70+
},
71+
data(): IData {
72+
return {
73+
otp: ``,
74+
otpSent: false,
75+
iti: null,
76+
phoneNumber: ``,
77+
inputCode: ``,
78+
isLoading: false,
79+
waitingForFunds: false,
80+
}
81+
},
82+
mounted() {
83+
const input = document.querySelector(`#phoneNumber`)
84+
if (input) {
85+
this.iti = intlTelInput(input, {
86+
utilsScript: require(`intl-tel-input/build/js/utils`),
87+
// any initialisation options go here
88+
})
89+
}
90+
},
91+
methods: {
92+
async sendOTP() {
93+
if (this.iti === null) {
94+
return
95+
}
96+
this.phoneNumber = this.iti.getNumber()
97+
if (!this.iti.isValidNumber()) {
98+
this.$toastError(`Invalid phone number`)
99+
return
100+
}
101+
this.isLoading = true
102+
await requestOTP(this.phoneNumber)
103+
this.otpSent = true
104+
this.$toastSuccess(
105+
`If you haven't used this phone number before on Blogchain, you'll receive a code on your phone`,
106+
)
107+
this.isLoading = false
108+
},
109+
async validateOTP() {
110+
try {
111+
if (this.otp.length !== 6) {
112+
this.$toastError(`OTP should have 6 digits`)
113+
return
114+
}
115+
if (!this.accountId) {
116+
return
117+
}
118+
this.isLoading = true
119+
await requestPhoneOnboard(this.phoneNumber, this.otp, this.accountId)
120+
this.isLoading = false
121+
this.waitingForFunds = true
122+
const { balance } = await waitForFunds(this.accountId)
123+
this.waitingForFunds = false
124+
this.$emit(`setIsOnboarded`, true)
125+
this.$emit(`updateFunds`, balance)
126+
} catch (err: any) {
127+
this.isLoading = false
128+
this.waitingForFunds = false
129+
if (err instanceof AxiosError && err.response) {
130+
this.otp = ``
131+
this.$toastError(err.response.data.error)
132+
return
133+
}
134+
this.$toastError(err.message)
135+
throw err
136+
}
137+
},
138+
},
139+
})
140+
</script>
141+
<style>
142+
.iti {
143+
width: 100%;
144+
}
145+
</style>

src/pages/register.vue

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,20 @@ import Vue from 'vue'
4747
import { mapMutations } from 'vuex'
4848
import { AxiosError } from 'axios'
4949
import CustomAuth from '@toruslabs/customauth'
50-
5150
import RegisterMethods from '@/components/register/RegisterMethods.vue'
52-
5351
import InfosPopup from '@/components/register/InfosPopup.vue'
5452
import SignUp from '@/components/register/SignUp.vue'
55-
5653
import CapsuleIcon from '@/components/icons/CapsuleNew.vue'
5754
5855
import { MutationType, namespace as sessionStoreNamespace } from '~/store/session'
59-
6056
import { removeNearPrivateKey, walletLogout } from '@/backend/near'
6157
import { ValidationError } from '@/errors'
6258
import { getAccountIdFromPrivateKey, getUserInfo, IWalletStatus } from '@/backend/auth'
6359
import { domain, torusNetwork } from '@/backend/utilities/config'
6460
import { revokeDiscordKey } from '@/backend/discordRevoke'
6561
62+
import 'intl-tel-input/build/css/intlTelInput.css'
63+
6664
interface IData {
6765
userInfo: null | IWalletStatus
6866
isLoading: boolean

0 commit comments

Comments
 (0)