Integrate Socialconnect SDK with Celo Composer for Social Payment

Integrate Socialconnect SDK with Celo Composer for Social Payment https://celo.academy/uploads/default/optimized/2X/0/00ed5f2f76cdff7964b1e39c7c6f551c2e82ab79_2_1024x576.jpeg
none 0.0 0

Introduction

This tutorial will show you how configure Socialconnect SDK, Register and deregister user phone number through Socialconnect SDK and seend payment to user phone number (Involve resolving phone number to wallet address using Socialconnect SDK).

Prerequisites

The assumption made in this tutorial is that you are already familiar with solidity and are aware of how smart contracts operate. It will be expected that you are familiar with how to use your preferred web3 tooling to deploy smart contracts to a network (Hardhat, Truffle, etc). Due to Celo’s compatibility with the Ethereum Virtual Machine (EVM), all the languages and workflows you learnt when developing for other EVM chains also apply to Celo.

  • Basic knowledge of JavaScript and ReactJS.
  • Understanding of the basics of blockchain and Ethereum.
  • Knowledge of Solidity, the programming language for writing smart contracts on CELO.
  • A wallet for interacting with the CELO network, such as Celo Wallet Extension, Valora etc.
  • Understanding of concepts like smart contract deployment, transactions, gas, and Web3 providers.

Getting Started

To bootstrap our React DAPP, we will be using Celo Composer.

Celo Composer

Celo Composer allows you to quickly build, deploy, and iterate on decentralized applications using Celo. It provides several frameworks, examples, and Celo-specific functionality to help you get started with your next dApp.

You can check out Celo Composer here https://github.com/celo-org/celo-composer/tree/main/packages/react-app.

To start our React App Project, we will use the Celo Composer CLI; the CLI makes it easy for us to select the options that we want when bootstrapping our dApp. Run this on your terminal

npx @celo/celo-composer@latest create
  • If you don’t have celo-compoer installed press enter to continue

  • Choose React when asked for the framework

  • Then choose React Celo for the web3 library

Choose hardhat (Only Hardhat is available at the time of writing this tutorial)

  • Skip subgraph, we won’t use it in this tutorial.

Your Project will now be created; you can check to make sure it has the following folders

  • packages/hardhat - Your Hardhat Folder - Where you can keep your Contracts
  • packages/react-app - Your React project

Install additional dependencies.

In order to configure Socialconnect SDK, we need to install some dependencies in our react app project.

yarn add @tailwindcss/forms @tailwindcss/typography bignumber.js @next/font react-hot-toast react-modal @celo/identity blind-threshold-bls@https://github.com/celo-org/blind-threshold-bls-wasm#3d1013a --exact

Copy blind_threshold_bls_bg.wasm into the /public folder of your web project, so that it’s accessible via an absolute path. Ensure that its location matches the path specified in the init function in the WebBlsBlindingClient that is used.

Source: Socialconnect SDK

Set up your issuer (read “Authentication” section in privacy.md), which is the account registering attestations. When a user requests for the issuer to register an attestation, the issuer should verify somehow that the user owns their identifier (ex. SMS verification for phone number identifiers).

Manage Environmental Variables

Create .env file to manage project environmental variables and update the file with the following variables

NEXT_PUBLIC_ISSUER_PRIVATE_KEY=
NEXT_PUBLIC_ISSUER_ADDRESS=

NEXT_PUBLIC_TWILIO_URL=

NEXT_PUBLIC_DEK_PUBLIC_KEY=
NEXT_PUBLIC_DEK_PRIVATE_KEY=

In order to send sms to user for verification we will be using twilio. Follow this link to setup twilio.

Manage Twilio Backend

Let’s create a file to structure all necessary function related to twilio like send-sms, verify otp etc. At the root of our react app folder, create a folder named services then create a file twilio.ts and pasted the below code snippet in it.

export const E164_REGEX = /^\+[1-9][0-9]{1,14}$/;

export function validatePhoneNumber(phoneNumber:string):boolean {
  if (E164_REGEX.test(phoneNumber)) {
    return true
  }
  return false
}

export async function sendSmsVerificationToken(phoneNumber: string) {
  try {
    if (!validatePhoneNumber(phoneNumber)) {
      throw "Attempting to hash a non-e164 number: " + phoneNumber;
    }

    const data = JSON.stringify({
      to: phoneNumber,
      channel: "sms",
    });

    const response = await fetch(
      `${process.env.NEXT_PUBLIC_TWILIO_URL}/start-verify`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: data,
      }
    );
    console.log("Verification request response:", response);
  } catch (error) {
    throw `Failed SMS verification: ${error}`;
  }
}

export async function verifyToken(
  phoneNumber: string,
  receivedCode: string
): Promise<boolean> {
  try {
    const data = JSON.stringify({
      to: phoneNumber,
      code: receivedCode,
    });
    const response = await fetch(
      `${process.env.NEXT_PUBLIC_TWILIO_URL}/check-verify`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: data,
      }
    );

    const json = await response.json();
    console.log("verification response", json.success);
    return json.success;
  } catch (error) {
    console.error(error);
    return false;
  }
}

Manage Components

In components folder, we need to create a reusable component for toast, let’s create a file toast.ts inside the components folder update the file with the code below

import toast from 'react-hot-toast';

type ToastType = 'error' | 'warning' | 'success';
const ToastComponent = ({
  type,
  title,
  description,
  onDismiss,
}: {
  type: ToastType;
  title: string;
  description?: string;
  onDismiss: () => void;
}) => (
  <div className="w-full py-3 px-1">
    <div className="flex items-start">
      <div className="flex-shrink-0">
        {type === 'warning' && (
          <svg
            className="h-6 w-6 text-yellow-400"
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="2"
              d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
            />
          </svg>
        )}
        {type === 'error' && (
          <svg
            className="h-6 w-6 text-red-400"
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="2"
              d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
            />
          </svg>
        )}
        {type === 'success' && (
          <svg
            className="h-6 w-6 text-green-400"
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
            aria-hidden="true"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="2"
              d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
            />
          </svg>
        )}
      </div>
      <div className="ml-3 w-0 flex-1 pt-0.5">
        <p className="text-sm font-medium text-gray-800">{title}</p>
        {description && (
          <p className="mt-1 text-sm text-gray-600">{description}</p>
        )}
      </div>
      <div className="ml-4 flex-shrink-0 flex">
        <button
          onClick={onDismiss}
          className="rounded-md inline-flex text-gray-700 hover:text-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
        >
          <span className="sr-only">Close</span>
          {/* <!-- Heroicon name: x --> */}
          <svg
            className="h-5 w-5"
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 20 20"
            fill="currentColor"
            aria-hidden="true"
          >
            <path
              fillRule="evenodd"
              d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
              clipRule="evenodd"
            />
          </svg>
        </button>
      </div>
    </div>
  </div>
);

const createToast = (type: ToastType, title: string, description?: string) =>
  toast(
    (t) => (
      <ToastComponent
        type={type}
        title={title}
        description={description}
        onDismiss={() => toast.dismiss(t.id)}
      />
    ),
    {
      duration: 5000,
    }
  );

export const error = (message: string, description?: string): string =>
  createToast('error', message, description);
export const warning = (message: string, description?: string): string =>
  createToast('warning', message, description);
export const success = (message: string, description?: string): string =>
  createToast('success', message, description);

Then lastly, we need a file to manage bls client, create a file name bls-blinding-client.ts

import { randomBytes } from 'crypto'
import thresholdBls from 'blind-threshold-bls'

export interface BlsBlindingClient {
  blindMessage: (base64PhoneNumber: string, seed?: Buffer) => Promise<string>
  unblindAndVerifyMessage: (blindedMessage: string) => Promise<string>
}

// The following interfaces should match https://github.com/celo-org/blind-threshold-bls-wasm/blob/master/src/blind_threshold_bls.d.ts

interface ThresholdBlsLib {
  blind: (message: Uint8Array, seed: Uint8Array) => BlindedMessage
  unblind: (blindedSignature: Uint8Array, blindingFactor: Uint8Array) => Uint8Array
  verify: (publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => void // throws on failure
}

interface BlindedMessage {
  blindingFactor: Uint8Array
  message: Uint8Array
}

export default class WebBlsBlindingClient implements BlsBlindingClient {
  private odisPubKey: Uint8Array
  private blindedValue: BlindedMessage | undefined
  private rawMessage: Buffer | undefined

  constructor(odisPubKey: string) {
    this.odisPubKey = Buffer.from(odisPubKey, 'base64')
  }

  async init() {
    await thresholdBls.init("/blind_threshold_bls_bg.wasm")
  }

  async blindMessage(base64PhoneNumber: string, seed?: Buffer): Promise<string> {
    const userSeed = seed ?? randomBytes(32)
    if (!seed) {
      console.warn(
        'Warning: Use a private deterministic seed (e.g. DEK private key) to preserve user quota when requests are replayed.'
      )
    }
    this.rawMessage = Buffer.from(base64PhoneNumber, 'base64')
    this.blindedValue = await thresholdBls.blind(this.rawMessage, userSeed)
    const blindedMessage = this.blindedValue.message
    return Buffer.from(blindedMessage).toString('base64')
  }

  async unblindAndVerifyMessage(base64BlindSig: string): Promise<string> {
    if (!this.rawMessage || !this.blindedValue) {
      throw new Error('Must call blind before unblinding')
    }

    const blindedSignature = Buffer.from(base64BlindSig, 'base64')
    const unblindMessage = await thresholdBls.unblind(
      blindedSignature,
      this.blindedValue.blindingFactor
    )
    // this throws on error
    await thresholdBls.verify(this.odisPubKey, this.rawMessage, unblindMessage)
    return Buffer.from(unblindMessage).toString('base64')
  }

  private isReactNativeEnvironment(): boolean {
    return typeof navigator !== 'undefined' && navigator.product === 'ReactNative'
  }
}

For user to be able to register/link their phone number with wallet address, let’s create a modal where user can input and verify their phone number. Create new file in components, name it RegisterNumberModal.tsx

import Modal from "react-modal";
import { useState } from "react";
import {
  sendSmsVerificationToken,
  validatePhoneNumber,
  verifyToken,
} from "../services/twilio";

export default function RegisterNumberModal({
  isOpen,
  onDismiss,
  registerNumber,
}: {
  isOpen: boolean;
  onDismiss: () => void;
  registerNumber: (number: string) => Promise<void>;
}) {
  const [activeIndex, setActiveIndex] = useState(0);

  const [number, setNumber] = useState("");
  const [userCode, setUserCode] = useState("");

  const [invalidInput, setInvalidInput] = useState(false);
  const [doneLoading, setDoneLoading] = useState(false);

  function editNumber(input: string) {
    setInvalidInput(false);
    setNumber(input);
  }

  function editCode(input: string) {
    setInvalidInput(false);
    setUserCode(input);
  }

  async function sendVerificationText() {
    if (!validatePhoneNumber(number)) {
      setInvalidInput(true);
      return;
    }
    await sendSmsVerificationToken(number);
    setInvalidInput(false);
    setActiveIndex(1);
  }

  async function validateCode() {
    const successfulVerification = await verifyToken(number, userCode);
    if (successfulVerification) {
      setActiveIndex(2);
      await registerNumber(number);
      setDoneLoading(true);
    } else {
      setInvalidInput(true);
    }
  }

  function closeModal() {
    setActiveIndex(0);
    setNumber("");
    setUserCode("");
    setDoneLoading(false);
    setInvalidInput(false);
    onDismiss();
  }

  const customStyles = {
    content: {
      margin: "10%",
      borderRadius: "0px",
      padding: "0",
      boxShadow: "0",
      background: "#FCF6F1",
      border: '1px solid #CCCCCC',
      height: "fit-content",
    },
  };

  return (
    <Modal isOpen={isOpen} style={customStyles}>
      {activeIndex === 0 ? (
        <div className="">
          <div className="p-20">
            <h2 className="py-5">Verify your phone number</h2>

            <label
              htmlFor="numberToRegister"
              className="block text-sm font-medium text-gray-700"
            >
              Phone number
            </label>
            <input
              type="text"
              name="numberToRegister"
              id="numberToRegister"
              value={number}
              onChange={(e) => editNumber(e.target.value)}
              className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-celo-green focus:ring-celo-green sm:text-sm"
            />
            {invalidInput && (
              <small>
                Not a valid phone number! Make sure you include the country code
              </small>
            )}
          </div>
          <div className="object-bottom bg-gray-50 px-4 py-3 text-right sm:px-6 ">
            <button
              className="inline-flex items-center rounded-full border border-wood bg-prosperity py-2 px-10 text-md font-medium text-black hover:bg-snow"
              onClick={sendVerificationText}
            >
              Verify
            </button>
          </div>
        </div>
      ) : activeIndex === 1 ? (
        <div className="">
          <div className="p-20">
            <h2 className="py-5">Enter the code we sent to your number</h2>
            <label
              htmlFor="userCode"
              className="block text-sm font-medium text-gray-700"
            >
              Verification Code
            </label>
            <input
              type="text"
              name="userCode"
              id="userCode"
              value={userCode}
              onChange={(e) => editCode(e.target.value)}
              className="mt-1 block w-full border-black focus:border-black sm:text-sm"
            />
            {invalidInput && (
              <small>
                Incorrect code! Make sure you&apos;re entering the latest code
                received to your phone
              </small>
            )}
          </div>
          <div className="object-bottom bg-gray-50 px-4 py-3 text-right sm:px-6 ">
            <button
              className="mr-3 inline-flex object-bottom justify-center border py-2 px-4 text-sm font-medium text-white shadow-sm focus:outline-none"
              onClick={validateCode}
            >
              Validate Code
            </button>
          </div>
        </div>
      ) : activeIndex === 2 ? (
        <div className="flex flex-col items-center">
          <h2 className="py-5">Registering your phone number</h2>
          {!doneLoading ? (
            <svg
              aria-hidden="true"
              className="mr-2 my-10 w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600 self-center"
              viewBox="0 0 100 101"
              fill="none"
              xmlns="http://www.w3.org/2000/svg"
            >
              <path
                d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
                fill="currentColor"
              />
              <path
                d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
                fill="currentFill"
              />
            </svg>
          ) : (
            <p className="my-10">Done!</p>
          )}
        </div>
      ) : null}

      <button
        className="mt-5 mr-3 absolute top-2 right-3 py-2 px-4 text-md font-medium text-black"
        onClick={closeModal}
      >
        Close
      </button>
    </Modal>
  );
}

Also let’s do same thing for de-register, create a file name DeregisterNumberModal.tsx

import Modal from "react-modal";
import { useState } from "react";
import { validatePhoneNumber } from "../services/twilio";

export default function DeregisterNumberModal({
  isOpen,
  onDismiss,
  deregisterNumber,
}: {
  isOpen: boolean;
  onDismiss: () => void;
  deregisterNumber: (number: string) => Promise<void>;
}) {
  const [activeIndex, setActiveIndex] = useState(0);

  const [number, setNumber] = useState("");

  const [invalidInput, setInvalidInput] = useState(false);
  const [doneLoading, setDoneLoading] = useState(false);

  function editNumber(input: string) {
    setInvalidInput(false);
    setNumber(input);
  }

  async function deregister() {
    if (!validatePhoneNumber(number)) {
      setInvalidInput(true);
      return;
    }
    // TODO: check that phone number has actually been linked to user's account
    setInvalidInput(false);
    setActiveIndex(1);
    await deregisterNumber(number);
    setDoneLoading(true);
  }

  function closeModal() {
    setActiveIndex(0);
    setNumber("");
    setDoneLoading(false);
    setInvalidInput(false);
    onDismiss();
  }

  const customStyles = {
    content: {
      margin: "10%",
      borderRadius: "0px",
      padding: "0",
      boxShadow: "0",
      background: "#FCF6F1",
      border: '1px solid #CCCCCC',
      height: "fit-content",
    },
  };

  return (
    <Modal isOpen={isOpen} style={customStyles}>
      {activeIndex === 0 ? (
        <div className="">
          <div className="p-20">
            <h2 className="py-5">De-register a phone number</h2>

            <label
              htmlFor="number"
              className="block text-sm font-medium text-gray-700"
            >
              Phone number
            </label>
            <input
              type="text"
              name="number"
              id="number"
              value={number}
              onChange={(e) => editNumber(e.target.value)}
              className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-celo-green focus:ring-celo-green sm:text-sm"
            />
            {invalidInput && (
              <small>
                Not a valid phone number! Make sure you include the country code
              </small>
            )}
          </div>

          <div className="object-bottom bg-gray-50 px-4 py-3 text-right sm:px-6 ">
            <button
              className="inline-flex items-center rounded-full border border-wood bg-prosperity py-2 px-10 text-md font-medium text-black hover:bg-snow"
              onClick={deregister}
            >
              De-register
            </button>
          </div>
        </div>
      ) : activeIndex === 1 ? (
        <div className="flex flex-col items-center">
          <h2 className="py-5">De-registering {number}</h2>
          {!doneLoading ? (
            <svg
              aria-hidden="true"
              className="mr-2 my-10 w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600 self-center"
              viewBox="0 0 100 101"
              fill="none"
              xmlns="http://www.w3.org/2000/svg"
            >
              <path
                d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
                fill="currentColor"
              />
              <path
                d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
                fill="currentFill"
              />
            </svg>
          ) : (
            <p className="my-10">Done!</p>
          )}
        </div>
      ) : null}

      <button
        className="mt-5 mr-3 absolute top-2 right-3 py-2 px-4 text-md font-medium text-black"
        onClick={closeModal}
      >
        Close
      </button>
    </Modal>
  );
}

Then a payment modal that allow user to input other user phone number in order to send payment to them, create file name SendToNumberModal.tsx

import Modal from "react-modal";
import { useState } from "react";
import { validatePhoneNumber } from "../services/twilio";
import BigNumber from "bignumber.js";

export default function SendToNumberModal({
  isOpen,
  onDismiss,
  sendToNumber,
}: {
  isOpen: boolean;
  onDismiss: () => void;
  sendToNumber: (number: string, amount: string) => Promise<void>;
}) {
  const [activeIndex, setActiveIndex] = useState(0);

  const [number, setNumber] = useState("");
  const [sendAmount, setSendAmount] = useState("");

  const [invalidInput, setInvalidInput] = useState(false);
  const [doneLoading, setDoneLoading] = useState(false);

  function editNumber(input: string) {
    setInvalidInput(false);
    setNumber(input);
  }

  function editAmount(input: string) {
    setInvalidInput(false);
    setSendAmount(input);
  }

  async function checkPhoneNumber() {
    if (!validatePhoneNumber(number)) {
      setInvalidInput(true);
      return;
    }
    // TODO: choose from linked addresses
    setInvalidInput(false);
    setActiveIndex(1);
  }

  async function send() {
    if (!new BigNumber(sendAmount).isGreaterThan(0)) {
      setInvalidInput(true);
      return;
    }
    setActiveIndex(2);
    await sendToNumber(number, sendAmount);
    setDoneLoading(true);
  }

  function closeModal() {
    setActiveIndex(0);
    setNumber("");
    setSendAmount("");
    setDoneLoading(false);
    setInvalidInput(false);
    onDismiss();
  }

  const customStyles = {
    content: {
      margin: "10%",
      borderRadius: "0px",
      padding: "0",
      boxShadow: "0",
      background: "#FCF6F1",
      border: '1px solid #CCCCCC',
      height: "fit-content",
    },
  };

  return (
    <Modal isOpen={isOpen} style={customStyles}>
      {activeIndex === 0 ? (
        <div className="">
          <div className="p-20">
            <h2 className="py-5">Recipient phone number</h2>
            <label
              htmlFor="number"
              className="block text-sm font-medium text-gray-700"
            >
              Phone number
            </label>
            <input
              type="text"
              name="number"
              id="number"
              value={number}
              onChange={(e) => editNumber(e.target.value)}
              className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-celo-green focus:ring-celo-green sm:text-sm"
            />
            {invalidInput && (
              <small>
                Not a valid phone number! Make sure you include the country code
              </small>
            )}
          </div>

          <div className="object-bottom bg-gray-50 px-4 py-3 text-right sm:px-6 ">
            <button
              className="inline-flex items-center rounded-full border border-wood bg-prosperity py-2 px-10 text-md font-medium text-black hover:bg-snow"
              onClick={checkPhoneNumber}
            >
              Send
            </button>
          </div>
        </div>
      ) : activeIndex === 1 ? (
        <div className="">
          <div className="p-20">
            <h2 className="py-5">Amount to send</h2>
            <label
              htmlFor="amount"
              className="block text-sm font-medium text-gray-700"
            >
              CELO
            </label>
            <input
              type="text"
              name="amount"
              id="amount"
              value={sendAmount}
              onChange={(e) => editAmount(e.target.value)}
              className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-celo-green focus:ring-celo-green sm:text-sm"
            />
            {invalidInput && (
              <small>Invalid amount! Needs to be a positive amount</small>
            )}
          </div>
          <div className="object-bottom bg-gray-50 px-4 py-3 text-right sm:px-6 ">
            <button
              className="mr-3 inline-flex object-bottom justify-center rounded-md border border-transparent bg-celo-green py-2 px-4 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-celo-green focus:ring-offset-2"
              onClick={send}
            >
              Send
            </button>
          </div>
        </div>
      ) : activeIndex === 2 ? (
        <div className="flex flex-col items-center">
          <h2 className="py-5">
            Sending {sendAmount} CELO to {number}
          </h2>
          {!doneLoading ? (
            <svg
              aria-hidden="true"
              className="mr-2 my-10 w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600 self-center"
              viewBox="0 0 100 101"
              fill="none"
              xmlns="http://www.w3.org/2000/svg"
            >
              <path
                d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
                fill="currentColor"
              />
              <path
                d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
                fill="currentFill"
              />
            </svg>
          ) : (
            <p className="my-10">Done!</p>
          )}
        </div>
      ) : null}

      <button
        className="mt-5 mr-3 absolute top-2 right-3 py-2 px-4 text-md font-medium text-black"
        onClick={closeModal}
      >
        Close
      </button>
    </Modal>
  );
}

Manage Page

Let’s update the index page with code below, open pages in the root folder of our react app then update the ```index.tsx`` file with the code below

import { ContractKit, newKit } from "@celo/contractkit";
import { BigNumber } from "bignumber.js";
import { useEffect, useState } from "react";
import { toast } from "../components";
import { OdisUtils } from "@celo/identity";
import WebBlsBlindingClient from "../components/bls-blinding-client";
import { useCelo } from "@celo/react-celo";
import "@celo/react-celo/lib/styles.css";
import { E164_REGEX } from "../services/twilio";
import { Account } from "web3-core";
import {
  AuthSigner,
  getServiceContext,
  OdisContextName,
} from "@celo/identity/lib/odis/query";
import { FederatedAttestationsWrapper } from "@celo/contractkit/lib/wrappers/FederatedAttestations";
import { OdisPaymentsWrapper } from "@celo/contractkit/lib/wrappers/OdisPayments";
import RegisterNumberModal from "./components/RegisterNumberModal";
import SendToNumberModal from "./components/SendToNumberModal";
import DeregisterNumberModal from "./components/DeregisterNumberModal";
import { IdentifierPrefix } from "@celo/identity/lib/odis/identifier";

function App() {

  let [componentInitialized, setComponentInitialized] = useState(false);
  const { initialised, kit, connect, address, destroy, network } = useCelo();

  const ISSUER_PRIVATE_KEY = process.env.NEXT_PUBLIC_ISSUER_PRIVATE_KEY;
  const DEK_PRIVATE_KEY = process.env.NEXT_PUBLIC_DEK_PRIVATE_KEY;
  let issuerKit: ContractKit,
    issuer: Account,
    federatedAttestationsContract: FederatedAttestationsWrapper,
    odisPaymentContract: OdisPaymentsWrapper;

  const [isRegisterNumberModalOpen, setIsRegisterNumberModalOpen] =
    useState(false);
  const [isSendToNumberModalOpen, setIsSendToNumberModalOpen] = useState(false);
  const [isDeregisterNumberModalOpen, setIsDeregisterNumberModalOpen] =
    useState(false);

  useEffect(() => {
    if (initialised) {
      setComponentInitialized(true);
    }
  }, [initialised]);

  useEffect(() => {
    const intializeIssuer = async () => {
      issuerKit = newKit(network.rpcUrl);
      issuer =
        issuerKit.web3.eth.accounts.privateKeyToAccount(ISSUER_PRIVATE_KEY);
      issuerKit.addAccount(ISSUER_PRIVATE_KEY);
      issuerKit.defaultAccount = issuer.address;
      federatedAttestationsContract =
        await issuerKit.contracts.getFederatedAttestations();
      odisPaymentContract = await issuerKit.contracts.getOdisPayments();
    };
    intializeIssuer();
  });

  async function getIdentifier(phoneNumber: string) {
    try {
      if (!E164_REGEX.test(phoneNumber)) {
        throw "Attempting to hash a non-e164 number: " + phoneNumber;
      }
      const ONE_CENT_CUSD = issuerKit.web3.utils.toWei("0.01", "ether");

      let authMethod: any = OdisUtils.Query.AuthenticationMethod.ENCRYPTION_KEY;
      const authSigner: AuthSigner = {
        authenticationMethod: authMethod,
        rawKey: DEK_PRIVATE_KEY,
      };

      const serviceContext = getServiceContext(OdisContextName.ALFAJORES);

      //check remaining quota
      const { remainingQuota } = await OdisUtils.Quota.getPnpQuotaStatus(
        issuer.address,
        authSigner,
        serviceContext
      );

      //increase quota if needed.
      console.log("remaining ODIS quota", remainingQuota);
      if (remainingQuota < 1) {
        // give odis payment contract permission to use cUSD
        const cusd = await issuerKit.contracts.getStableToken();
        const currrentAllowance = await cusd.allowance(
          issuer.address,
          odisPaymentContract.address
        );
        console.log("current allowance:", currrentAllowance.toString());
        let enoughAllowance: boolean = false;

        if (currrentAllowance < BigNumber(ONE_CENT_CUSD)) {
          const approvalTxReceipt = await cusd
            .increaseAllowance(odisPaymentContract.address, ONE_CENT_CUSD)
            .sendAndWaitForReceipt();
          console.log("approval status", approvalTxReceipt.status);
          enoughAllowance = approvalTxReceipt.status;
        } else {
          enoughAllowance = true;
        }

        // increase quota
        if (enoughAllowance) {
          const odisPayment = await odisPaymentContract
            .payInCUSD(issuer.address, ONE_CENT_CUSD)
            .sendAndWaitForReceipt();
          console.log("odis payment tx status:", odisPayment.status);
          console.log("odis payment tx hash:", odisPayment.transactionHash);
        } else {
          throw "cUSD approval failed";
        }
      }

      const blindingClient = new WebBlsBlindingClient(
        serviceContext.odisPubKey
      );
      await blindingClient.init();
      console.log("fetching identifier for:", phoneNumber);
      const response = await OdisUtils.Identifier.getObfuscatedIdentifier(
        phoneNumber,
        IdentifierPrefix.PHONE_NUMBER,
        issuer.address,
        authSigner,
        serviceContext,
        undefined,
        undefined,
        blindingClient
      );

      console.log(`Obfuscated phone number: ${response.obfuscatedIdentifier}`);

      console.log(
        `Obfuscated phone number is a result of: sha3('tel://${response.plaintextIdentifier}__${response.pepper}') => ${response.obfuscatedIdentifier}`
      );

      return response.obfuscatedIdentifier;
    } catch (error) {
      throw `failed to get identifier: ${error}`;
    }
  }

  // this function needs to be called once when using a new issuer address for the first time
  async function registerIssuerAccountAndDEK() {
    if (issuer.address == undefined) {
      throw "issuer not found";
    }
    const accountsContract = await issuerKit.contracts.getAccounts();

    // register account if needed
    let registeredAccount = await accountsContract.isAccount(issuer.address);
    if (!registeredAccount) {
      console.log("Registering account");
      const receipt = await accountsContract
        .createAccount()
        .sendAndWaitForReceipt({ from: issuer.address });
      console.log("Receipt status: ", receipt.status);
    } else {
      console.log("Account already registered");
    }

    // register DEK
    const DEK_PUBLIC_KEY = process.env.NEXT_PUBLIC_DEK_PUBLIC_KEY;
    console.log("registering dek");
    await accountsContract
      .setAccountDataEncryptionKey(DEK_PUBLIC_KEY)
      .sendAndWaitForReceipt({ from: issuer.address });
    console.log("dek registered");
  }

  async function registerNumber(number: string) {
    try {
      const verificationTime = Math.floor(new Date().getTime() / 1000);

      const identifier = await getIdentifier(number);
      console.log(identifier);

      // TODO: lookup list of issuers per phone number.
      // This could be a good example to have for potential issuers to learn about this feature.

      const { accounts } =
        await federatedAttestationsContract.lookupAttestations(identifier, [
          issuer.address,
        ]);
      console.log(accounts);

      if (accounts.length == 0) {
        const attestationReceipt = await federatedAttestationsContract
          .registerAttestationAsIssuer(identifier, address, verificationTime)
          .sendAndWaitForReceipt();
        console.log("attestation Receipt status:", attestationReceipt.status);
        console.log(
          `Register Attestation as issuer TX hash: ${network.explorer}/tx/${attestationReceipt.transactionHash}/internal-transactions`
        );
      } else {
        console.log("phone number already registered with this issuer");
      }
    } catch (error) {
      throw `Error registering phone number: ${error}`;
    }
  }

  async function sendToNumber(number: string, amount: string) {
    try {
      const identifier = await getIdentifier(number);
      const amountInWei = issuerKit.web3.utils.toWei(amount, "ether");

      const attestations =
        await federatedAttestationsContract.lookupAttestations(identifier, [
          issuer.address,
        ]);

      // TODO: handle when no accounts mapped to number

      const CELO = await kit.contracts.getGoldToken();
      await CELO.transfer(
        attestations.accounts[0],
        amountInWei
      ).sendAndWaitForReceipt({ gasPrice: 20000000000 });
    } catch (error) {
      throw `Failed to send funds to ${number}: ${error}`;
    }
  }

  async function deregisterPhoneNumber(phoneNumber: string) {
    try {
      const identifier = await getIdentifier(phoneNumber);
      const receipt = await federatedAttestationsContract
        .revokeAttestation(identifier, issuer.address, address)
        .sendAndWaitForReceipt();
      console.log(
        `revoke attestation transaction receipt status: ${receipt.status}`
      );
    } catch (error) {
      throw `Failed to deregister phone number: ${error}`;
    }
  }

  return (
    <main>
      <br />
      {componentInitialized && address ? (
        <div className="flex flex-col mx-auto content-center">
          <p className="flex flex-col mx-auto content-center">Connected address (user):</p> 
          <p>{address}</p>
          <button
              type="button"
              className="inline-flex self-center items-center rounded-full border border-wood bg-prosperity py-5 px-10 my-5 text-md font-medium text-black hover:bg-snow"
              onClick={destroy}
          >Disconnect user</button>

          <div className="my-0 sm:mt-0">
            <div className="overflow-hidden">
              <div className="bg-fig border-x border-t border-lavender px-4 py-5 text-center sm:px-6">
                <button
                  className="mr-3 inline-flex justify-center py-2 px-4 text-sm font-medium text-white hover:text-sand"
                  onClick={() => setIsRegisterNumberModalOpen(true)}
                >
                  Verify and register your phone number
                </button>
              </div>
            </div>
          </div>

          <div className="my-0 sm:mt-0">
            <div className="overflow-hidden">
              <div className="bg-fig border-x border-t border-lavender px-4 py-5 text-center sm:px-6">
                <button
                  className="mr-3 inline-flex justify-center py-2 px-4 text-sm font-medium text-white hover:text-sand"
                  onClick={() => setIsSendToNumberModalOpen(true)}
                >
                  Send payment to a phone number
                </button>
              </div>
            </div>
          </div>

          <div className="my-0 sm:mt-0">
            <div className="overflow-hidden">
              <div className="bg-fig border-x border-t border-b border-lavender px-4 py-5 text-center sm:px-6">
                <button
                  className="mr-3 inline-flex justify-center py-2 px-4 text-sm font-medium text-white hover:text-sand"
                  onClick={() => setIsDeregisterNumberModalOpen(true)}
                >
                  De-register your phone number
                </button>
              </div>
            </div>
          </div>

          <RegisterNumberModal
            isOpen={isRegisterNumberModalOpen}
            onDismiss={() => setIsRegisterNumberModalOpen(false)}
            registerNumber={registerNumber}
          />
          <SendToNumberModal
            isOpen={isSendToNumberModalOpen}
            onDismiss={() => setIsSendToNumberModalOpen(false)}
            sendToNumber={sendToNumber}
          />
          <DeregisterNumberModal
            isOpen={isDeregisterNumberModalOpen}
            onDismiss={() => setIsDeregisterNumberModalOpen(false)}
            deregisterNumber={deregisterPhoneNumber}
          />
        </div>        
      ) : (
        <button
            type="button"
            className="inline-flex items-center rounded-full border border-wood bg-prosperity py-5 px-10 text-md font-medium text-black hover:bg-snow"
            onClick={() =>
                connect().catch((e) => console.log((e as Error).message))
            }
        >Connect user</button>
      )}
    </main>
  );
}

export default App;

Let’s test our react app, open your terminal and run the command below

yarn dev

Demo Url

Conclusion

Congratulations, you have now learned how to integrate Socialconnect SDK with Celo Composer. Send Token to user using their phone number.

About the Author

I am a Software Engineer, Tech Evangelist (Preaching the gospel of flutter & blockchain) also and Ex-GDSC Leads.

References

7 Likes

Would you be making use of soulnames?

3 Likes

Approved, congrats on winning the hackathon!

4 Likes

Hi @mujhtech i will be reviewing this

3 Likes

A great piece here.

please what is the url you used?

2 Likes

Welcome to Celo community @GideonAde

4 Likes

Thanks for your diligent moderation @EmiriDbest

4 Likes

Nice tutorial @mujhtech … and congratulations once more on winning the Hackathon.

4 Likes

Nice work :+1::+1:

Good work!:+1:

1 Like

@EmiriDbest there is a link mentioned in the tutorial on how do set up twilio backend in which you can use the URL for your frontend

2 Likes

You can get more information regarding some constant value here https://github.com/celo-org/SocialConnect/blob/main/examples/constants.ts

2 Likes

Hi @Clue, I have noticed your comments in several places are often similar to this. It sounds like a bot action. Could you please take your time to read through any post you choose, give constructive feedback or alternate your comments to avoid looking spammy?

cc: @jimohyusufolatunji4 others

7 Likes

I’ve been following his comments plus few other ones. It is turning to spam

2 Likes

Changing Github to GitHub