Skip to content

Commit

Permalink
Feat/7 add new form to send transactions (#9)
Browse files Browse the repository at this point in the history
* update dependencies and add form component for transfer funds

* add new account transfer form for sending transactions with solana

* add token info endpoint for loading the token decimals

* fix rpc url for devnet

* fix transaction issues
  • Loading branch information
jdnichollsc authored Mar 1, 2024
1 parent a91bbc8 commit a50614e
Show file tree
Hide file tree
Showing 21 changed files with 689 additions and 207 deletions.
10 changes: 8 additions & 2 deletions apps/webapp/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig } from '@angular/core';
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideClientHydration } from '@angular/platform-browser';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideWalletAdapter } from '@heavy-duty/wallet-adapter';
import { SweetAlert2Module } from '@sweetalert2/ngx-sweetalert2';

import { appRoutes } from './app.routes';

Expand All @@ -12,7 +13,12 @@ export const appConfig: ApplicationConfig = {
provideClientHydration(),
provideRouter(appRoutes),
provideAnimationsAsync(),
provideWalletAdapter(),
provideWalletAdapter({
autoConnect: !!localStorage.getItem('autoConnect'),
}),
provideHttpClient(),
importProvidersFrom([
SweetAlert2Module.forRoot(),
])
],
};
4 changes: 2 additions & 2 deletions apps/webapp/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export const appRoutes: Route[] = [
},
{
path: 'account',
loadComponent: () =>
import('./pages/account/account.page').then((m) => m.AccountPage),
loadChildren: () =>
import('./pages/account/account.module').then((m) => m.AccountPageRoutingModule),
},
{
path: '',
Expand Down
36 changes: 36 additions & 0 deletions apps/webapp/src/app/components/account/send/send.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<webapp-forms-transfer
*ngIf="(publicKey$ | async) as publicKey; else elsePublicKey"
[isLoading]="!!(isSendTransactionLoading && isSendTransactionLoading())"
(submitForm)="onSubmitForm($event, publicKey)"
/>

<ng-template #elsePublicKey>
<div *ngIf="(connected$ | async) as connected; else disconnected" class="mt-4 flex justify-center">
<mat-spinner diameter="32"></mat-spinner>
</div>
<ng-template #disconnected>
<p class="text-gray-500 p-5">
Please connect your wallet to be able to send transactions.
</p>
</ng-template>
</ng-template>

<swal
#errorSwal
title="Ops! Something went wrong"
[text]="errorMessage"
icon="error"
[showCancelButton]="false"
[focusCancel]="false"
/>

<swal
#confirmationSwal
title="The transaction has been sent, yay! 🎉"
text="Your transaction has been sent successfully. You can check the status of your transaction in the transaction history."
icon="success"
[showCancelButton]="false"
[focusCancel]="false"
confirmButtonText="Go to transaction history"
(confirm)="onGoToTransactionHistory()"
/>
85 changes: 85 additions & 0 deletions apps/webapp/src/app/components/account/send/send.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Component, effect, inject, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { CommonModule } from '@angular/common';
import { SwalComponent, SweetAlert2Module } from '@sweetalert2/ngx-sweetalert2';
import { PublicKey } from '@solana/web3.js';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import Swal from 'sweetalert2';

import { WalletStore } from '../../../store';
import {
TransferForm,
TransferFormComponent,
} from '../../forms/transfer/transfer.component';
import { environment } from '../../../../environments/environment';

@Component({
selector: 'webapp-account-send',
standalone: true,
imports: [
CommonModule,
TransferFormComponent,
SweetAlert2Module,
MatProgressSpinnerModule,
],
templateUrl: './send.component.html',
providers: [WalletStore],
})
export class SendComponent implements OnInit {
readonly walletStore = inject(WalletStore);
readonly isSendTransactionLoading = this.walletStore.isLoading;
readonly publicKey$ = this.walletStore.wallet().publicKey$;
readonly connected$ = this.walletStore.wallet().connected$;
errorMessage = '';
@ViewChild('errorSwal')
public readonly errorSwal!: SwalComponent;
@ViewChild('confirmationSwal')
public readonly confirmationSwal!: SwalComponent;

constructor(private router: Router) {
effect(() => {
if (this.walletStore.error && this.walletStore.error()) {
const error = this.walletStore.error();
console.error('error', error);
this.errorMessage =
error || 'An error occurred while sending transaction';
this.errorSwal.fire();
}
});
effect(() => {
if (this.walletStore.signature && this.walletStore.signature()) {
console.warn('signature', this.walletStore.signature());
this.confirmationSwal.fire();
}
});
}

ngOnInit() {
this.walletStore.clearSignature();
}

async onSubmitForm(payload: TransferForm, publicKey: PublicKey) {
console.log('onSubmitForm', payload);
this.walletStore.sendTransaction({
amount: payload.amount,
memo: payload.memo,
tokenAddress: environment.mintUSDC,
senderAddress: publicKey.toBase58(),
receiverAddress: payload.receiver,
});

Swal.fire({
title: 'Loading...',
html: 'Please wait...',
allowEscapeKey: false,
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
},
});
}

onGoToTransactionHistory() {
this.router.navigate(['/account']);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@

<mat-tab-group preserveContent backgroundColor="primary">
<mat-tab label="Wallet Balance" class="text-black">
<div
*ngIf="(account$ | async) as account; else elseAccount"
class="rounded-lg bg-gray-50 px-4 py-6 sm:flex sm:items-center sm:justify-between sm:space-x-6 sm:px-6 lg:space-x-8">
<dl
class="flex-auto space-y-6 divide-y divide-gray-200 text-sm text-gray-600 sm:grid sm:grid-cols-3 sm:gap-x-6 sm:space-y-0 sm:divide-y-0 lg:w-1/2 lg:flex-none lg:gap-x-8">
<div class="flex justify-between sm:block">
<dd class="sm:mt-1">
<img [src]="account.info.image" alt="account.info.name"
class="h-16 w-16 rounded object-cover object-center" />
</dd>
</div>
<div class="flex justify-between pt-6 sm:block sm:pt-0">
<dt class="font-medium text-gray-900">Balance</dt>
<dd class="sm:mt-1">{{ account.balance }}</dd>
</div>
<div class="flex justify-between pt-6 font-medium text-gray-900 sm:block sm:pt-0">
<dt>Is Frozen</dt>
<dd class="sm:mt-1">
{{ account.isFrozen ? 'Yes' : 'No' }}
</dd>
</div>
</dl>
</div>
<ng-template #elseAccount>
<div *ngIf="(connected$ | async) as connected; else disconnected" class="mt-4 flex justify-center">
<mat-spinner diameter="32"></mat-spinner>
</div>
<ng-template #disconnected>
<p class="text-gray-500 p-5">
Please connect your wallet to see your balance.
</p>
</ng-template>
</ng-template>
</mat-tab>
<mat-tab label="Transaction History" class="text-black">
<p *ngIf="(connected$ | async) === false" class="text-gray-500 p-5">
Please connect your wallet to see your history.
</p>
<div *ngIf="!!isTransactionsLoading?.()" class="mt-4 flex justify-center">
<mat-spinner diameter="32"></mat-spinner>
</div>
<table *ngIf="!!transactions().length && (connected$ | async)" class="mt-4 w-full text-gray-500 sm:mt-6 table-auto">
<thead class="text-left text-sm text-gray-500">
<tr>
<th scope="col" class="py-3 pr-8 font-normal">
Type
</th>
<th scope="col" class="hidden sm:w-2/5 lg:w-1/3 py-3 pr-8 font-normal sm:table-cell">
Memo
</th>
<th scope="col" class="py-3 pr-8 font-normal sm:table-cell">
Amount
</th>
<th scope="col" class="w-0 py-3 font-normal">
Date
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 border-b border-gray-200 text-sm sm:border-t">
<tr class="text-medium items-center" *ngFor="let transaction of transactions()">
<td class="py-6 pr-8">
<div class="flex items-center">
<div>
<div class="font-medium text-gray-900">
{{ transaction.type }}
</div>
</div>
</div>
</td>
<td class="hidden py-6 pr-8 sm:table-cell text-gray-900">
{{ transaction.memo }}
</td>
<td class="py-6 pr-8 sm:table-cell text-gray-900 font-bold text-lg flex flex-row items-center gap-3 h-full align-middle">
{{ transaction.amount }}
<ng-icon
*ngIf="transaction.sign !== undefined"
[name]="transaction.sign > 1 ? 'heroArrowUp' : 'heroArrowDown'"
class="h-4 w-4"
[ngClass]="transaction.sign > 1 ? 'text-green-500' : 'text-red-500'"
aria-hidden="true"
/>
</td>
<td class="py-6 text-gray-900">
{{ transaction.timestamp | date: 'short' }}
</td>
</tr>
</tbody>
</table>
<div *ngIf="error && error()?.length" class="mt-4">
<div class="text-red-600">{{ error() }}</div>
<button (click)="loadTransactions()" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">
Try again
</button>
</div>
</mat-tab>
</mat-tab-group>
32 changes: 32 additions & 0 deletions apps/webapp/src/app/components/account/wallet/wallet.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { heroArrowUp, heroArrowDown } from '@ng-icons/heroicons/outline';
import { NgIconComponent, provideIcons } from '@ng-icons/core';

import { WalletStore } from '../../../store';

@Component({
selector: 'webapp-account-wallet',
standalone: true,
imports: [CommonModule, MatTabsModule, MatProgressSpinnerModule, NgIconComponent],
templateUrl: './wallet.component.html',
providers: [WalletStore, provideIcons({ heroArrowUp, heroArrowDown })],
})
export class WalletComponent implements OnInit {
readonly walletStore = inject(WalletStore);
readonly account$ = this.walletStore.account();
readonly connected$ = this.walletStore.wallet().connected$;
readonly transactions = this.walletStore.transactions;
readonly isTransactionsLoading = this.walletStore.isLoading;
readonly error = this.walletStore.error;

ngOnInit() {
this.loadTransactions();
}

loadTransactions() {
this.walletStore.loadTransactions(10);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="flex flex-col items-center gap-5">
<mat-form-field appearance="fill" subscriptSizing="dynamic" class="w-full">
<mat-label>Memo</mat-label>
<input matInput placeholder="Memo" formControlName="memo" required />
<mat-icon matSuffix>description</mat-icon>
<mat-error *ngIf="form.get('memo')?.hasError('required')">
Memo is requiredf
</mat-error>
</mat-form-field>

<mat-form-field appearance="fill" subscriptSizing="dynamic" class="w-full">
<mat-label>Amount</mat-label>
<input type="number" matInput placeholder="Memo" formControlName="amount" required />
<mat-icon matSuffix>attach_money</mat-icon>
<mat-error *ngIf="form.get('amount')?.hasError('required')">
Amount is required
</mat-error>
</mat-form-field>

<mat-form-field appearance="fill" subscriptSizing="dynamic" class="w-full">
<mat-label>Receiver</mat-label>
<input matInput placeholder="Receiver" formControlName="receiver" required />
<mat-icon matSuffix>key</mat-icon>
<mat-error *ngIf="form.get('receiver')?.hasError('required')">
Receiver is required
</mat-error>
</mat-form-field>

<footer>
<button
type="submit"
mat-raised-button
color="primary"
class="self-end w-auto rounded-md bg-indigo-600 px-10 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Transfer
</button>
</footer>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { MatError, MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';

export type TransferForm = {
memo: string;
amount: number;
receiver: string;
};

@Component({
selector: 'webapp-forms-transfer',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
FormsModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatError,
],
templateUrl: './transfer.component.html',
})
export class TransferFormComponent implements OnInit {
@Input() isLoading = false;
@Output() readonly submitForm = new EventEmitter<TransferForm>();

readonly form = new FormGroup({
memo: new FormControl('', Validators.required),
amount: new FormControl<number | undefined>(undefined, Validators.required),
receiver: new FormControl('', Validators.required),
});

ngOnInit() {
this.form.setValue({
memo: '',
amount: null,
receiver: '',
});
}

onSubmit() {
if (this.form.invalid) return;
this.submitForm.emit({
memo: this.form.value.memo || '',
amount: this.form.value.amount || 0,
receiver: this.form.value.receiver || '',
});
}
}
Loading

0 comments on commit a50614e

Please sign in to comment.