Ruka kwenda kwenye maudhui makuu

Kutengeneza kiolesura cha mtumiaji kwa ajili ya mkataba wako

TypeScript
React
Vite
Wagmi
frontend
Mwanzo
Ori Pomerantz
1 Novemba 2023
13 soma ndani ya dakika

Umepata kipengele tunachohitaji katika mfumo ikolojia wa Ethereum. Uliandika mikataba-erevu ili kuitekeleza, na labda hata msimbo fulani unaohusiana unaoendeshwa nje ya mnyororo. Hii ni nzuri! Kwa bahati mbaya, bila kiolesura cha mtumiaji hutapata watumiaji wowote, na mara ya mwisho ulipoandika tovuti watu walitumia modemu za kupiga simu na JavaScript ilikuwa mpya.

Makala hii ni kwa ajili yako. Nadhani unajua programu, na labda kidogo ya JavaScript na HTML, lakini ujuzi wako wa kiolesura cha mtumiaji umeshuka na umepitwa na wakati. Kwa pamoja tutapitia programu rahisi ya kisasa ili uone jinsi inavyofanywa siku hizi.

Kwa nini hii ni muhimu

Kinadharia, unaweza tu kuwa na watu wanaotumia Etherscan (opens in a new tab) au Blockscout (opens in a new tab) kuingiliana na mikataba yako. Hiyo itakuwa nzuri kwa WanaEthereum wenye uzoefu. Lakini tunajaribu kuwahudumia watu wengine bilioni moja (opens in a new tab). Hili halitatokea bila uzoefu mzuri wa mtumiaji, na kiolesura rafiki cha mtumiaji ni sehemu kubwa ya hiyo.

Programu ya Greeter

Kuna nadharia nyingi nyuma ya jinsi UI ya kisasa inavyofanya kazi, na tovuti nyingi nzuri (opens in a new tab) zinazoelezea (opens in a new tab). Badala ya kurudia kazi nzuri iliyofanywa na tovuti hizo, nitachukulia unapendelea kujifunza kwa kufanya na kuanza na programu unayoweza kucheza nayo. Bado unahitaji nadharia ili kufanikisha mambo, na tutaifikia - tutapitia faili chanzo kwa faili chanzo, na kujadili mambo tunapoyafikia.

Usakinishaji

  1. Ikibidi, ongeza blockchain ya Holesky (opens in a new tab) kwenye mkoba wako na pata ETH ya majaribio (opens in a new tab).

  2. Fanya clone ya hazina ya github.

    1git clone https://github.com/qbzzt/20230801-modern-ui.git
  3. Sakinisha vifurushi vinavyohitajika.

    1cd 20230801-modern-ui
    2pnpm install
  4. Anzisha programu.

    1pnpm dev
  5. Vinjari hadi kwenye URL inayoonyeshwa na programu. Katika hali nyingi, hiyo ni http://localhost:5173/ (opens in a new tab).

  6. Unaweza kuona msimbo chanzo wa mkataba, toleo lililobadilishwa kidogo la Greeter ya Hardhat, kwenye kichunguzi cha blockchain (opens in a new tab).

Mapitio ya faili

index.html

Faili hii ni kiolezo cha kawaida cha HTML isipokuwa kwa mstari huu, unaoingiza faili ya hati.

1<script type="module" src="/src/main.tsx"></script>

src/main.tsx

Kiendelezi cha faili kinatuambia kuwa faili hii ni kipengele cha React (opens in a new tab) kilichoandikwa kwa TypeScript (opens in a new tab), kiendelezi cha JavaScript kinachosaidia ukaguzi wa aina (opens in a new tab). TypeScript inakusanywa kuwa JavaScript, kwa hivyo tunaweza kuitumia kwa utekelezaji upande wa mteja.

1import '@rainbow-me/rainbowkit/styles.css'
2import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
3import * as React from 'react'
4import * as ReactDOM from 'react-dom/client'
5import { WagmiConfig } from 'wagmi'
6import { chains, config } from './wagmi'

Ingiza msimbo wa maktaba tunaouhitaji.

1import { App } from './App'

Ingiza kipengele cha React kinachotekeleza programu (tazama hapa chini).

1ReactDOM.createRoot(document.getElementById('root')!).render(

Tengeneza kipengele cha msingi cha React. Kigezo cha render ni JSX (opens in a new tab), lugha ya kiendelezi inayotumia HTML na JavaScript/TypeScript. Alama ya mshangao hapa inakiambia kipengele cha TypeScript: "hujui kama document.getElementById('root') itakuwa kigezo halali kwa ReactDOM.createRoot, lakini usijali - mimi ni msanidi programu na ninakuambia itakuwa hivyo".

1 <React.StrictMode>

Programu inaingia ndani ya kipengele cha React.StrictMode (opens in a new tab). Kipengele hiki kinaambia maktaba ya React kuingiza ukaguzi wa ziada wa utatuzi, ambao ni muhimu wakati wa usanidi.

1 <WagmiConfig config={config}>

Programu pia iko ndani ya kipengele cha WagmiConfig (opens in a new tab). Maktaba ya wagmi (we are going to make it) (opens in a new tab) inaunganisha ufafanuzi wa UI wa React na maktaba ya viem (opens in a new tab) kwa ajili ya kuandika mfumo uliotawanywa wa Ethereum.

1 <RainbowKitProvider chains={chains}>

Na mwishowe, kipengele cha RainbowKitProvider (opens in a new tab). Kipengele hiki hushughulikia kuingia na mawasiliano kati ya mkoba na programu.

1 <App />

Sasa tunaweza kuwa na kipengele cha programu, ambacho kinatengeneza UI. Ile /> mwishoni mwa kipengele inaiambia React kwamba kipengele hiki hakina ufafanuzi wowote ndani yake, kulingana na kiwango cha XML.

1 </RainbowKitProvider>
2 </WagmiConfig>
3 </React.StrictMode>,
4)

Bila shaka, tunapaswa kufunga vipengele vingine.

src/App.tsx

1import { ConnectButton } from '@rainbow-me/rainbowkit'
2import { useAccount } from 'wagmi'
3import { Greeter } from './components/Greeter'
4
5export function App() {

Hii ndiyo njia ya kawaida ya kuunda kipengele cha React - fafanua chaguo la kukokotoa ambalo huitwa kila wakati linapohitaji kutolewa. Chaguo hili la kukokotoa kwa kawaida huwa na msimbo fulani wa TypeScript au JavaScript juu, ikifuatiwa na taarifa ya return inayorejesha msimbo wa JSX.

1 const { isConnected } = useAccount()

Hapa tunatumia useAccount (opens in a new tab) kuangalia kama tumeunganishwa kwenye blockchain kupitia mkoba au la.

Kwa kimkataba, katika chaguo za kukokotoa za React zinazoitwa use... ni hooks (opens in a new tab) zinazorejesha aina fulani ya data. Unapotumia ndoano kama hizo, si tu kwamba kipengele chako kinapata data, lakini data hiyo inapobadilika kipengele hutolewa upya na maelezo yaliyosasishwa.

1 return (
2 <>

JSX ya kipengele cha React lazima irudishe kipengele kimoja. Tunapokuwa na vipengele vingi na hatuna chochote kinachomaliza "kawaida" tunatumia kipengele tupu (<> ... </>) ili kuzifanya kuwa sehemu moja.

1 <h1>Greeter</h1>
2 <ConnectButton />

Tunapata kipengele cha ConnectButton (opens in a new tab) kutoka kwa RainbowKit. Wakati hatujaunganishwa, inatupa kitufe cha Connect Wallet ambacho hufungua modali inayoelezea pochi na kukuruhusu kuchagua ni ipi unayotumia. Tunapounganishwa, inaonyesha blockchain tunayotumia, anwani ya akaunti yetu, na salio letu la ETH. Tunaweza kutumia maonyesho haya kubadili mtandao au kukata muunganisho.

1 {isConnected && (

Tunapohitaji kuingiza JavaScript halisi (au TypeScript ambayo itakusanywa kwa JavaScript) kwenye JSX, tunatumia mabano ({}).

Sintaksia a && b ni fupi kwa a ? b : a (opens in a new tab). Yaani, ikiwa a ni kweli inatathmini kuwa b na vinginevyo inatathmini a (ambayo inaweza kuwa false, 0, n.k). Hii ni njia rahisi ya kuiambia React kwamba kipengele kinapaswa kuonyeshwa tu ikiwa sharti fulani limetimizwa.

Katika hali hii, tunataka tu kumwonyesha mtumiaji Greeter ikiwa mtumiaji ameunganishwa kwenye blockchain.

1 <Greeter />
2 )}
3 </>
4 )
5}

src/components/Greeter.tsx

Faili hii ina utendakazi mwingi wa UI. Inajumuisha ufafanuzi ambao kwa kawaida ungekuwa katika faili nyingi, lakini kwa kuwa hii ni mafunzo, programu imeboreshwa ili iwe rahisi kuelewa kwa mara ya kwanza, badala ya utendakazi au urahisi wa matengenezo.

1import { useState, ChangeEventHandler } from 'react'
2import { useNetwork,
3 useReadContract,
4 usePrepareContractWrite,
5 useContractWrite,
6 useContractEvent
7 } from 'wagmi'

Tunatumia vitendaji hivi vya maktaba. Tena, zimeelezwa hapa chini zinapotumiwa.

1import { AddressType } from 'abitype'

Maktaba ya abitype (opens in a new tab) inatupa ufafanuzi wa TypeScript kwa aina mbalimbali za data za Ethereum, kama vile AddressType (opens in a new tab).

1let greeterABI = [
2 .
3 .
4 .
5] as const // greeterABI

ABI kwa mkataba wa Greeter. Ikiwa unasanidi mikataba na UI kwa wakati mmoja, kwa kawaida unaweza kuziweka katika hazina moja na kutumia ABI iliyotolewa na mkusanyaji wa Solidity kama faili katika programu yako. Hata hivyo, hii si lazima hapa kwa sababu mkataba tayari umesanidiwa na hautabadilika.

1type AddressPerBlockchainType = {
2 [key: number]: AddressType
3}

TypeScript imeandikwa kwa nguvu. Tunatumia ufafanuzi huu kubainisha anwani ambamo mkataba wa Greeter umetumwa kwenye minyororo tofauti. Ufunguo ni nambari (chainId), na thamani ni AddressType (anwani).

1const contractAddrs: AddressPerBlockchainType = {
2 // Holesky
3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',
4
5 // Sepolia
6 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'
7}

Anwani ya mkataba kwenye mitandao miwili inayotumika: Holesky (opens in a new tab) na Sepolia (opens in a new tab).

Kumbuka: Kwa kweli kuna ufafanuzi wa tatu, kwa Redstone Holesky, utaelezwa hapa chini.

1type ShowObjectAttrsType = {
2 name: string,
3 object: any
4}

Aina hii hutumiwa kama kigezo kwa sehemu ya ShowObject (iliyoelezwa baadaye). Inajumuisha jina la kitu na thamani yake, ambayo huonyeshwa kwa madhumuni ya utatuzi.

1type ShowGreetingAttrsType = {
2 greeting: string | undefined
3}

Wakati wowote tunaweza kujua salamu ni nini (kwa sababu tuliisoma kutoka kwa blockchain) au hatujui (kwa sababu bado hatujaipokea). Kwa hivyo ni muhimu kuwa na aina ambayo inaweza kuwa mfuatano au chochote.

Kipengele cha Greeter
1const Greeter = () => {

Hatimaye, tunapata kufafanua kipengele.

1 const { chain } = useNetwork()

Taarifa kuhusu msururu tunaotumia, kwa hisani ya wagmi (opens in a new tab). Kwa sababu hii ni ndoano (use...), kila wakati habari hii inapobadilika sehemu hiyo huchorwa upya.

1 const greeterAddr = chain && contractAddrs[chain.id]

Anwani ya mkataba wa Greeter, ambayo hutofautiana kulingana na mnyororo (na ambayo ni undefined ikiwa hatuna maelezo ya mnyororo au tuko kwenye mnyororo bila mkataba huo).

1 const readResults = useReadContract({
2 address: greeterAddr,
3 abi: greeterABI,
4 functionName: "greet" , // No arguments
5 watch: true
6 })

Ndoano ya useReadContract (opens in a new tab) inasoma habari kutoka kwa mkataba. Unaweza kuona hasa ni taarifa gani inarejesha panua readResults katika UI. Katika kesi hii tunataka iendelee kutafuta ili tujulishwe salamu itakapobadilika.

Kumbuka: Tunaweza kusikiliza matukio ya setGreeting (opens in a new tab) ili kujua salamu inapobadilika na kusasisha kwa njia hiyo. Hata hivyo, ingawa inaweza kuwa na ufanisi zaidi, haitatumika katika hali zote. Mtumiaji anapobadilisha hadi msururu tofauti, salamu pia hubadilika, lakini mabadiliko hayo hayaambatani na tukio. Tunaweza kuwa na sehemu moja ya msimbo inayosikiliza matukio na nyingine ya kutambua mabadiliko ya msururu, lakini hilo lingekuwa gumu zaidi kuliko kuweka tu kigezo cha watch (opens in a new tab).

1 const [ newGreeting, setNewGreeting ] = useState("")

Ndoano ya useState ya React (opens in a new tab) inaturuhusu kubainisha tofauti ya hali, ambayo thamani yake hudumu kutoka uwasilishaji mmoja wa sehemu hadi nyingine. Thamani ya awali ni kigezo, katika kesi hii mfuatano tupu.

Ndoano ya useState inarudisha orodha yenye thamani mbili:

  1. Thamani ya sasa ya tofauti ya hali.
  2. Chaguo la kukokotoa la kurekebisha tofauti ya hali inapohitajika. Kwa kuwa hii ni ndoano, kila wakati inapoitwa sehemu hutolewa tena.

Katika kesi hii, tunatumia tofauti ya hali kwa salamu mpya ambayo mtumiaji anataka kuweka.

1 const greetingChange : ChangeEventHandler<HTMLInputElement> = (evt) =>
2 setNewGreeting(evt.target.value)

Hiki ndicho kishughulikia tukio wakati sehemu mpya ya kuingiza salamu inapobadilika. Aina, ChangeEventHandler<HTMLInputElement> (opens in a new tab), inabainisha kuwa huyu ni mshughulikiaji wa mabadiliko ya thamani ya kipengele cha ingizo cha HTML. Sehemu ya <HTMLInputElement> inatumika kwa sababu hii ni aina ya jumla (opens in a new tab).

1 const preparedTx = usePrepareContractWrite({
2 address: greeterAddr,
3 abi: greeterABI,
4 functionName: 'setGreeting',
5 args: [ newGreeting ]
6 })
7 const workingTx = useContractWrite(preparedTx.config)

Huu ndio mchakato wa kuwasilisha muamala wa blockchain kutoka kwa mtazamo wa mteja:

  1. Tuma muamala kwa nodi katika blockchain kwa kutumia eth_estimateGas (opens in a new tab).
  2. Subiri jibu kutoka kwa nodi.
  3. Jibu linapopokewa, mwombe mtumiaji asaini muamala kupitia mkoba. Hatua hii lazima itokee baada ya jibu la nodi kupokelewa kwa sababu mtumiaji anaonyeshwa gharama ya gesi ya muamala kabla ya kuusaini.
  4. Subiri mtumiaji akubali.
  5. Tuma muamala tena, wakati huu ukitumia eth_sendRawTransaction (opens in a new tab).

Hatua ya 2 inaweza kuchukua muda unaoonekana, ambapo watumiaji wangeshangaa kama amri yao ilipokelewa na kiolesura cha mtumiaji na kwa nini hawaombwi kutia saini muamala tayari. Hiyo inafanya uzoefu mbaya wa mtumiaji (UX).

Suluhisho ni kutumia ndoano za kuandaa (opens in a new tab). Kila wakati kigezo kinapobadilika, tuma ombi la eth_estimateGas kwa nodi mara moja. Kisha, mtumiaji anapotaka kutuma muamala (katika kesi hii kwa kubonyeza Sasisha salamu), gharama ya gesi inajulikana na mtumiaji anaweza kuona ukurasa wa mkoba mara moja.

1 return (

Sasa hatimaye tunaweza kuunda HTML halisi ya kurudisha.

1 <>
2 <h2>Greeter</h2>
3 {
4 !readResults.isError && !readResults.isLoading &&
5 <ShowGreeting greeting={readResults.data} />
6 }
7 <hr />

Unda sehemu ya ShowGreeting (iliyoelezwa hapa chini), lakini tu ikiwa salamu ilisomwa kwa mafanikio kutoka kwa blockchain.

1 <input type="text"
2 value={newGreeting}
3 onChange={greetingChange}
4 />

Hiki ndicho sehemu ya maandishi ya kuingiza ambapo mtumiaji anaweza kuweka salamu mpya. Kila wakati mtumiaji anapobonyeza kitufe, tunaita greetingChange ambayo huita setNewGreeting. Kwa vile setNewGreeting inatoka kwa useState ndoano, inasababisha Greeter sehemu kutolewa tena. Hii inamaanisha kuwa:

  • Tunahitaji kubainisha value ili kuweka thamani ya salamu mpya, kwa sababu vinginevyo ingerejea kuwa chaguo-msingi, mfuatano tupu.
  • usePrepareContractWrite inaitwa kila wakati newGreeting inapobadilika, ambayo ina maana kwamba daima itakuwa na newGreeting ya hivi karibuni zaidi katika muamala ulioandaliwa.
1 <button disabled={!workingTx.write}
2 onClick={workingTx.write}
3 >
4 Sasisha salamu
5 </button>

Ikiwa hakuna workingTx.write basi bado tunasubiri maelezo muhimu kwa kutuma sasisho la salamu, kwa hivyo kitufe kimezimwa. Ikiwa kuna thamani ya workingTx.write basi hiyo ndiyo kazi ya kuita ili kutuma muamala.

1 <hr />
2 <ShowObject name="readResults" object={readResults} />
3 <ShowObject name="preparedTx" object={preparedTx} />
4 <ShowObject name="workingTx" object={workingTx} />
5 </>
6 )
7}

Mwishowe, kukusaidia kuona tunachofanya, onyesha vitu vitatu tunavyotumia:

  • readResults
  • preparedTx
  • workingTx
Kipengele cha ShowGreeting

Sehemu hii inaonyesha

1const ShowGreeting = (attrs : ShowGreetingAttrsType) => {

Chaguo la kukokotoa la sehemu hupokea kigezo chenye sifa zote za sehemu.

1 return <b>{attrs.greeting}</b>
2}
Kipengele cha ShowObject

Kwa madhumuni ya habari, tunatumia ShowObject sehemu kuonyesha vitu muhimu (readResults kwa kusoma salamu na preparedTx na workingTx kwa miamala tunayounda).

1const ShowObject = (attrs: ShowObjectAttrsType ) => {
2 const keys = Object.keys(attrs.object)
3 const funs = keys.filter(k => typeof attrs.object[k] == "function")
4 return <>
5 <details>

Hatutaki kubandika UI na habari zote, kwa hivyo ili iwezekane kuzitazama au kuzifunga, tunatumia lebo ya details (opens in a new tab).

1 <summary>{attrs.name}</summary>
2 <pre>
3 {JSON.stringify(attrs.object, null, 2)}

Sehemu nyingi huonyeshwa kwa kutumia JSON.stringify (opens in a new tab).

1 </pre>
2 { funs.length > 0 &&
3 <>
4 Functions:
5 <ul>

Isipokuwa ni vitendaji, ambavyo si sehemu ya kiwango cha JSON (opens in a new tab), kwa hivyo lazima zionyeshwe kando.

1 {funs.map((f, i) =>

Ndani ya JSX, msimbo ndani ya { mabano ya curly } hutafsiriwa kama JavaScript. Kisha, msimbo ndani ya ( mabano ya kawaida ), hutafsiriwa tena kama JSX.

1 (<li key={i}>{f}</li>)
2 )}

React inahitaji lebo katika DOM Tree (opens in a new tab) ili kuwa na vitambulisho tofauti. Hii inamaanisha kuwa watoto wa lebo moja (katika kesi hii, orodha isiyopangwa (opens in a new tab)), wanahitaji sifa tofauti za key.

1 </ul>
2 </>
3 }
4 </details>
5 </>
6}

Maliza lebo mbalimbali za HTML.

Uuzaji wa mwisho
1export { Greeter }

Kipengele cha Greeter ndicho tunachohitaji kuhamisha kwa ajili ya programu.

src/wagmi.ts

Hatimaye, ufafanuzi mbalimbali unaohusiana na WAGMI uko katika src/wagmi.ts. Sitaeleza kila kitu hapa, kwa sababu sehemu kubwa yake ni kiolezo ambacho huenda huhitaji kubadilisha.

Msimbo hapa si sawa kabisa na kwenye github (opens in a new tab) kwa sababu baadaye katika makala tunaongeza mnyororo mwingine (Redstone Holesky (opens in a new tab)).

1import { getDefaultWallets } from '@rainbow-me/rainbowkit'
2import { configureChains, createConfig } from 'wagmi'
3import { holesky, sepolia } from 'wagmi/chains'

Ingiza blockchains ambazo programu inasaidia. Unaweza kuona orodha ya minyororo inayotumika kwenye github ya viem (opens in a new tab).

1import { publicProvider } from 'wagmi/providers/public'
2
3const walletConnectProjectId = 'c96e690bb92b6311e8e9b2a6a22df575'

Ili uweze kutumia WalletConnect (opens in a new tab) unahitaji kitambulisho cha mradi kwa ajili ya programu yako. Unaweza kuipata kwenye cloud.walletconnect.com (opens in a new tab).

1const { chains, publicClient, webSocketPublicClient } = configureChains(
2 [ holesky, sepolia ],
3 [
4 publicProvider(),
5 ],
6)
7
8const { connectors } = getDefaultWallets({
9 appName: 'My wagmi + RainbowKit App',
10 chains,
11 projectId: walletConnectProjectId,
12})
13
14export const config = createConfig({
15 autoConnect: true,
16 connectors,
17 publicClient,
18 webSocketPublicClient,
19})
20
21export { chains }

Kuongeza blockchain nyingine

Siku hizi kuna suluhisho nyingi za uongezaji wa L2, na unaweza kutaka kusaidia baadhi ambazo viem bado haizisaidii. Ili kuifanya, unarekebisha src/wagmi.ts. Maagizo haya yanaelezea jinsi ya kuongeza Redstone Holesky (opens in a new tab).

  1. Ingiza aina ya defineChain kutoka kwa viem.

    1import { defineChain } from 'viem'
  2. Ongeza ufafanuzi wa mtandao.

    1const redstoneHolesky = defineChain({
    2 id: 17_001,
    3 name: 'Redstone Holesky',
    4 network: 'redstone-holesky',
    5 nativeCurrency: {
    6 decimals: 18,
    7 name: 'Ether',
    8 symbol: 'ETH',
    9 },
    10 rpcUrls: {
    11 default: {
    12 http: ['https://rpc.holesky.redstone.xyz'],
    13 webSocket: ['wss://rpc.holesky.redstone.xyz/ws'],
    14 },
    15 public: {
    16 http: ['https://rpc.holesky.redstone.xyz'],
    17 webSocket: ['wss://rpc.holesky.redstone.xyz/ws'],
    18 },
    19 },
    20 blockExplorers: {
    21 default: { name: 'Explorer', url: 'https://explorer.holesky.redstone.xyz' },
    22 },
    23})
  3. Ongeza mnyororo mpya kwenye simu ya configureChains.

    1 const { chains, publicClient, webSocketPublicClient } = configureChains(
    2 [ holesky, sepolia, redstoneHolesky ],
    3 [ publicProvider(), ],
    4 )
  4. Hakikisha kwamba programu inajua anwani ya mikataba yako kwenye mtandao mpya. Katika kesi hii, tunarekebisha src/components/Greeter.tsx:

    1const contractAddrs : AddressPerBlockchainType = {
    2 // Holesky
    3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',
    4
    5 // Redstone Holesky
    6 17001: '0x4919517f82a1B89a32392E1BF72ec827ba9986D3',
    7
    8 // Sepolia
    9 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'
    10}

Hitimisho

Bila shaka, hujali sana kuhusu kutoa kiolesura cha mtumiaji kwa ajili ya Greeter. Unataka kuunda kiolesura cha mtumiaji kwa mikataba yako mwenyewe. Ili kuunda programu yako mwenyewe, fuata hatua hizi:

  1. Bainisha kuunda programu ya wagmi.

    1pnpm create wagmi
  2. Taja jina la programu.

  3. Chagua mfumo wa React.

  4. Chagua lahaja ya Vite.

  5. Unaweza kuongeza kit cha Rainbow (opens in a new tab).

Sasa nenda ukafanye mikataba yako itumike kwa ulimwengu wote.

Tazama hapa kwa kazi zangu zaidi (opens in a new tab).

Ukurasa ulihaririwa mwisho: 3 Machi 2026

Umesaidika na mafunzo haya?