diff --git a/frontend/src/app/components/faucet/faucet.component.html b/frontend/src/app/components/faucet/faucet.component.html index 1030087b1..349c51826 100644 --- a/frontend/src/app/components/faucet/faucet.component.html +++ b/frontend/src/app/components/faucet/faucet.component.html @@ -4,43 +4,45 @@

Testnet4 Faucet

- @if (error) { -
- @switch (error) { - @case ('faucet_too_soon') { - Too many requests! Try again later. - } - @case ('faucet_maximum_reached') { - You have exceeded your testnet4 allowance. Try again later. - } - @case ('faucet_not_available') { - The faucet is not available right now. Try again later. - } - @default { - Sorry, something went wrong! Try again later. - } - } -
- } -
- @if (txid) { -
- {{ txid }} -
- } @else if (loading) { -

Waiting for faucet...

+ + @if (loading) { +

Loading faucet...

- } @else { -
+ } @else if (!user) { + +
+ To limit abuse,  + authenticate  + or + +
+ } + @else if (error === 'not_available') { + +
+ To limit abuse,  + become a sponsor  + or + +
+ } + + @else if (error) { + + + } + + @if (!loading) { +
-
-
+
+
Amount (sats)
- +
@@ -49,43 +51,31 @@
Amount is required
-
Minimum is {{ amount?.errors?.['min'].min }}
-
Maximum is {{ amount?.errors?.['max'].max }}
+
Minimum is {{ amount?.errors?.['min'].min | number }} tSats
+
Maximum is {{ amount?.errors?.['max'].max | number }} tSats
-
+
Address
- - + +
- @if (address?.errors?.['required']) { -
Address is required
- } @else { -
Must be a valid testnet4 address
- } -
-
-
Too many requests! Try again later.
+
Address is required
+
Must be a valid testnet4 address
- @if (!user) { -
- To limit abuse, please log in or sign up and link your Twitter account to use the faucet. -
- } @else if (!status?.access) { -
- To use this feature, please link your Twitter account. -
- } } -
-
- If you no longer need your testnet4 coins, please consider sending them back to {{ status.address }} to replenish the faucet. -
+ + + @if (status?.address) { +
If you no longer need your testnet4 coins, please consider sending them back to replenish the faucet.
+ } +
-
+ +
\ No newline at end of file diff --git a/frontend/src/app/components/faucet/faucet.component.scss b/frontend/src/app/components/faucet/faucet.component.scss index 084168ca4..d611f5a23 100644 --- a/frontend/src/app/components/faucet/faucet.component.scss +++ b/frontend/src/app/components/faucet/faucet.component.scss @@ -23,6 +23,9 @@ .submit-button, .button-group, .button-group .btn { flex-grow: 1; } + .submit-button:disabled { + pointer-events: none; + } #satoshis::after { content: 'sats'; @@ -41,4 +44,9 @@ width: 100%; max-width: 800px; margin: auto; -} \ No newline at end of file +} + +.invalid { + border-width: 1px; + border-color: var(--red); +} diff --git a/frontend/src/app/components/faucet/faucet.component.ts b/frontend/src/app/components/faucet/faucet.component.ts index 98d7a0c57..afe9e9241 100644 --- a/frontend/src/app/components/faucet/faucet.component.ts +++ b/frontend/src/app/components/faucet/faucet.component.ts @@ -1,13 +1,13 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { FormBuilder, FormGroup, ValidationErrors, Validators } from "@angular/forms"; -import { StorageService } from '../../services/storage.service'; -import { ServicesApiServices } from '../../services/services-api.service'; -import { AudioService } from '../../services/audio.service'; -import { StateService } from '../../services/state.service'; -import { Subscription, tap } from "rxjs"; -import { HttpErrorResponse } from "@angular/common/http"; +import { Component, OnDestroy, OnInit, ChangeDetectorRef } from "@angular/core"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { Subscription } from "rxjs"; +import { StorageService } from "../../services/storage.service"; +import { ServicesApiServices } from "../../services/services-api.service"; import { getRegex } from "../../shared/regex.utils"; +import { StateService } from "../../services/state.service"; import { WebsocketService } from "../../services/websocket.service"; +import { AudioService } from "../../services/audio.service"; +import { HttpErrorResponse } from "@angular/common/http"; @Component({ selector: 'app-faucet', @@ -15,52 +15,75 @@ import { WebsocketService } from "../../services/websocket.service"; styleUrls: ['./faucet.component.scss'] }) export class FaucetComponent implements OnInit, OnDestroy { - user: any; - loading: boolean = true; + loading = true; + error: string = ''; + user: any = undefined; + txid: string = ''; + + faucetStatusSubscription: Subscription; status: { - address?: string, - access: boolean - min: number, - user_max: number, - user_requests: number, + min: number; // minimum amount to request at once (in sats) + max: number; // maximum amount to request at once + address?: string; // faucet address } | null = null; - error = ''; faucetForm: FormGroup; - txid = ''; mempoolPositionSubscription: Subscription; confirmationSubscription: Subscription; constructor( - private stateService: StateService, + private cd: ChangeDetectorRef, private storageService: StorageService, private servicesApiService: ServicesApiServices, - private websocketService: WebsocketService, - private audioService: AudioService, private formBuilder: FormBuilder, + private stateService: StateService, + private websocketService: WebsocketService, + private audioService: AudioService ) { + this.faucetForm = this.formBuilder.group({ + 'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4'))]], + 'satoshis': [0, [Validators.required, Validators.min(0), Validators.max(0)]] + }); } - ngOnInit(): void { + ngOnDestroy() { + if (this.faucetStatusSubscription) { + this.faucetStatusSubscription.unsubscribe(); + } + } + + ngOnInit() { this.user = this.storageService.getAuth()?.user ?? null; - this.initForm(5000, 500000); - if (this.user) { - try { - this.servicesApiService.getFaucetStatus$().subscribe(status => { - this.status = status; - this.initForm(this.status.min, this.status.user_max); - }) - } catch (e) { - if (e?.status !== 403) { - this.error = 'faucet_not_available'; - } - } finally { - this.loading = false; - } - } else { + if (!this.user) { this.loading = false; + return; } + // Setup form + this.faucetStatusSubscription = this.servicesApiService.getFaucetStatus$().subscribe({ + next: (status) => { + if (!status) { + this.error = 'internal_server_error'; + return; + } + this.status = status; + + this.faucetForm = this.formBuilder.group({ + 'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4'))]], + 'satoshis': [this.status.min, [Validators.required, Validators.min(this.status.min), Validators.max(this.status.max)]] + }); + + this.loading = false; + this.cd.markForCheck(); + }, + error: (response) => { + this.loading = false; + this.error = response.error; + this.cd.markForCheck(); + } + }); + + // Track transaction this.websocketService.want(['blocks', 'mempool-blocks']); this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => { if (txPosition && txPosition.txid === this.txid) { @@ -78,26 +101,6 @@ export class FaucetComponent implements OnInit, OnDestroy { }); } - initForm(min: number, max: number): void { - this.faucetForm = this.formBuilder.group({ - 'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4'))]], - 'satoshis': ['', [Validators.required, Validators.min(min), Validators.max(max)]] - }, { validators: (formGroup): ValidationErrors | null => { - if (this.status && !this.status?.user_requests) { - return { customError: 'You have used the faucet too many times already! Come back later.'} - } - return null; - }}); - this.faucetForm.get('satoshis').setValue(min); - this.loading = false; - } - - setAmount(value: number): void { - if (this.faucetForm) { - this.faucetForm.get('satoshis').setValue(value); - } - } - requestCoins(): void { this.error = null; this.stateService.markBlock$.next({}); @@ -114,23 +117,19 @@ export class FaucetComponent implements OnInit, OnDestroy { }); } - ngOnDestroy(): void { - this.stateService.markBlock$.next({}); - this.websocketService.stopTrackingTransaction(); - if (this.mempoolPositionSubscription) { - this.mempoolPositionSubscription.unsubscribe(); - } - if (this.confirmationSubscription) { - this.confirmationSubscription.unsubscribe(); + setAmount(value: number): void { + if (this.faucetForm) { + this.faucetForm.get('satoshis').setValue(value); } } get amount() { return this.faucetForm.get('satoshis')!; } - get address() { return this.faucetForm.get('address')!; } get invalidAmount() { const amount = this.faucetForm.get('satoshis')!; return amount?.invalid && (amount.dirty || amount.touched) } + + get address() { return this.faucetForm.get('address')!; } get invalidAddress() { const address = this.faucetForm.get('address')!; return address?.invalid && (address.dirty || address.touched) diff --git a/frontend/src/app/components/twitter-login/twitter-login.component.html b/frontend/src/app/components/twitter-login/twitter-login.component.html new file mode 100644 index 000000000..2fe2392e0 --- /dev/null +++ b/frontend/src/app/components/twitter-login/twitter-login.component.html @@ -0,0 +1,6 @@ + + + {{ buttonString }} + diff --git a/frontend/src/app/components/twitter-login/twitter-login.component.ts b/frontend/src/app/components/twitter-login/twitter-login.component.ts new file mode 100644 index 000000000..17583b00e --- /dev/null +++ b/frontend/src/app/components/twitter-login/twitter-login.component.ts @@ -0,0 +1,25 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +@Component({ + selector: 'app-twitter-login', + templateUrl: './twitter-login.component.html', +}) +export class TwitterLogin { + @Input() width: string | null = null; + @Input() customClass: string | null = null; + @Input() buttonString: string= 'unset'; + @Input() redirectTo: string | null = null; + @Output() clicked = new EventEmitter(); + @Input() disabled: boolean = false; + + constructor() {} + + twitterLogin() { + this.clicked.emit(true); + if (this.redirectTo) { + location.replace(`/api/v1/services/auth/login/twitter?redirectTo=${encodeURIComponent(this.redirectTo)}`); + } else { + location.replace(`/api/v1/services/auth/login/twitter?redirectTo=${location.href}`); + } + return false; + } +} diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index aec4be089..f57aa8524 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -161,7 +161,7 @@ export class ServicesApiServices { } getFaucetStatus$() { - return this.httpClient.get<{ address?: string, access: boolean, min: number, user_max: number, user_requests: number }>(`${SERVICES_API_PREFIX}/testnet4/faucet/status`, { responseType: 'json' }); + return this.httpClient.get<{ address?: string, min: number, max: number }>(`${SERVICES_API_PREFIX}/testnet4/faucet/status`, { responseType: 'json' }); } requestTestnet4Coins$(address: string, sats: number) { diff --git a/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts b/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts index 07b96427d..4bb86f16d 100644 --- a/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts +++ b/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts @@ -22,6 +22,9 @@ const MempoolErrors = { 'waitlisted': `You are currently on the wait list. You will get notified once you are granted access.`, 'not_whitelisted_by_any_pool': `You are not whitelisted by any mining pool`, 'unauthorized': `You are not authorized to do this`, + 'faucet_too_soon': `You cannot request any more coins right now. Try again later.`, + 'faucet_not_available': `The faucet is not available right now. Try again later.`, + 'faucet_maximum_reached': `You are not allowed to request more coins`, } as { [error: string]: string }; export function isMempoolError(error: string) { diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 89d62b375..777ca7180 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -115,6 +115,7 @@ import { BitcoinsatoshisPipe } from '../shared/pipes/bitcoinsatoshis.pipe'; import { HttpErrorComponent } from '../shared/components/http-error/http-error.component'; import { TwitterWidgetComponent } from '../components/twitter-widget/twitter-widget.component'; import { FaucetComponent } from '../components/faucet/faucet.component'; +import { TwitterLogin } from '../components/twitter-login/twitter-login.component'; import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-directives/weight-directives'; @@ -230,6 +231,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir HttpErrorComponent, TwitterWidgetComponent, FaucetComponent, + TwitterLogin, ], imports: [ CommonModule, @@ -359,6 +361,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir PendingStatsComponent, HttpErrorComponent, TwitterWidgetComponent, + TwitterLogin, MempoolBlockOverviewComponent, ClockchainComponent,