Merge branch 'master' into nymkappa/fido

This commit is contained in:
Felipe Knorr Kuhn 2025-01-23 01:08:25 -08:00 committed by GitHub
commit a2009aa322
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 156 additions and 10 deletions

View file

@ -45,6 +45,7 @@ __SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
# Export as environment variables to be used by envsubst
export __MAINNET_ENABLED__
@ -76,6 +77,7 @@ export __SERVICES_API__
export __PUBLIC_ACCELERATIONS__
export __HISTORICAL_PRICE__
export __ADDITIONAL_CURRENCIES__
export __STRATUM_ENABLED__
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
echo ${folder}

View file

@ -397,13 +397,13 @@
</div>
}
</div>
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
<p class="text-nowrap">&mdash;<span i18n="or">OR</span>&mdash;</p>
</div>
}
}
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<app-fiat [value]="cost"></app-fiat> with</p>
@if (canPayWithCashapp) {
@ -421,6 +421,13 @@
<img src="/resources/google-pay.png" height=37>
</div>
}
@if (canPayWithCardOnFile) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { <span class="mt-1 mb-1"></span> }
<div class="paymentMethod mx-2 d-flex justify-content-center align-items-center" style="width: 200px; height: 55px" (click)="moveToStep('cardonfile')">
<fa-icon style="font-size: 24px; color: white" [icon]="['fas', 'credit-card']"></fa-icon>
<span class="ml-2" style="font-size: 22px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
</div>
}
</div>
}
</div>
@ -443,7 +450,7 @@
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
</div>
</div>
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') {
<!-- Show checkout page -->
<div class="row mb-md-1 text-center" id="confirm-title">
<div class="col-sm" id="confirm-payment-title">
@ -459,7 +466,7 @@
</div>
</div>
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay || step === 'cardonfile' && !loadingCardOnFile) {
<div class="row text-center mt-1">
<div class="col-sm">
<div class="form-group w-100">
@ -484,8 +491,13 @@
<div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
} @else if (step === 'googlepay') {
<div id="google-pay-button" class="d-inline-block" style="height: 50px" [style]="loadingGooglePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
} @else if (step === 'cardonfile') {
<div class="paymentMethod mx-2 d-flex justify-content-center align-items-center ml-auto mr-auto" style="width: 200px; height: 55px" (click)="requestCardOnFilePayment()" [style]="loadingCardOnFile ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''">
<fa-icon style="font-size: 24px; color: white" [icon]="['fas', 'credit-card']"></fa-icon>
<span class="ml-2" style="font-size: 22px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
</div>
}
@if (loadingCashapp || loadingApplePay || loadingGooglePay) {
@if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) {
<div display="d-flex flex-row justify-content-center">
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>

View file

@ -13,7 +13,7 @@ import { EnterpriseService } from '@app/services/enterprise.service';
import { ApiService } from '@app/services/api.service';
import { isDevMode } from '@angular/core';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay' | 'cardOnFile';
export type AccelerationEstimate = {
hasAccess: boolean;
@ -26,7 +26,7 @@ export type AccelerationEstimate = {
mempoolBaseFee: number;
vsizeFee: number;
pools: number[];
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number, card?: {card_id: string, last_4: string, brand: string, name: string, billing: any}}>;
unavailable?: boolean;
options: { // recommended bid options
fee: number; // recommended userBid in sats
@ -49,7 +49,7 @@ export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2;
export const MAX_BID_RATIO = 4;
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success';
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'cardonfile' | 'processing' | 'paid' | 'success';
@Component({
selector: 'app-accelerate-checkout',
@ -65,6 +65,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() cashappEnabled: boolean = true;
@Input() applePayEnabled: boolean = false;
@Input() googlePayEnabled: boolean = true;
@Input() cardOnFileEnabled: boolean = true;
@Input() advancedEnabled: boolean = false;
@Input() forceMobile: boolean = false;
@Input() showDetails: boolean = false;
@ -117,6 +118,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
loadingCashapp = false;
loadingApplePay = false;
loadingGooglePay = false;
loadingCardOnFile = false;
payments: any;
cashAppPay: any;
applePay: any;
@ -234,6 +236,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingGooglePay = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'cardonfile' && this.cardOnFileEnabled) {
this.loadingCardOnFile = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'paid') {
this.timePaid = Date.now();
this.timeoutTimer = setTimeout(() => {
@ -454,6 +460,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
await this.requestApplePayPayment();
} else if (this._step === 'googlepay') {
await this.requestGooglePayPayment();
} else if (this._step === 'cardonfile') {
this.loadingCardOnFile = false;
}
},
error: () => {
@ -710,6 +718,109 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
);
}
/**
* Card On File
*/
async requestCardOnFilePayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
const costUSD = this.cost / 100_000_000 * conversions.USD;
if (this.isCheckoutLocked > 0) {
return;
}
const cardOnFile = this.estimate?.availablePaymentMethods?.cardOnFile;
if (!cardOnFile?.card) {
this.accelerateError = 'card_on_file_not_found';
return;
}
this.loadingCardOnFile = false;
try {
this.isCheckoutLocked += 2;
this.isTokenizing += 2;
const nameParts = cardOnFile.card.name.split(' ');
const assumedGivenName = nameParts[0];
const assumedFamilyName = nameParts.length > 1 ? nameParts[1] : undefined;
const verificationDetails = {
card: {
billing: {
givenName: assumedGivenName,
familyName: assumedFamilyName,
addressLines: [cardOnFile.card.billing.addressLine1],
city: cardOnFile.card.billing.locality,
state: cardOnFile.card.billing.administrativeDistrictLevel1,
countyCode: cardOnFile.card.billing.country,
}
}
};
const verificationToken = await this.$verifyBuyer(this.payments, cardOnFile.card.card_id, verificationDetails, costUSD.toFixed(2));
if (!verificationToken || !verificationToken.token) {
console.error(`SCA verification failed`);
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
this.processing = false;
return;
}
this.servicesApiService.accelerateWithCardOnFile$(
this.tx.txid,
cardOnFile.card.card_id,
verificationToken.token,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD,
verificationToken.userChallenged
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
setTimeout(() => {
this.isCheckoutLocked--;
this.isTokenizing--;
this.moveToStep('paid', true);
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
this.isCheckoutLocked--;
this.isTokenizing--;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}
}
});
} catch (e) {
console.log(e);
this.isCheckoutLocked--;
this.isTokenizing--;
this.processing = false;
this.accelerateError = e.message;
} finally {
// always unlock the checkout once we're finished
this.isCheckoutLocked--;
this.isTokenizing--;
}
}
);
}
/**
* CASHAPP
*/
@ -955,6 +1066,22 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
return false;
}
get canPayWithCardOnFile(): boolean {
if (!this.cardOnFileEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
const paymentMethod = this.estimate?.availablePaymentMethods?.cardOnFile;
if (paymentMethod) {
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
return true;
}
}
return false;
}
get canPayWithBalance(): boolean {
if (!this.hasAccessToBalanceMode) {
return false;

View file

@ -14,7 +14,7 @@
<th class="time text-right" i18n="accelerator.requested">Requested</th>
</ng-container>
<ng-container *ngIf="!pending">
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="fee text-right text-truncate" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="block text-right" i18n="shared.block-title">Block</th>
<th class="pool text-right" i18n="mining.pool-name" *ngIf="!this.widget">Pool</th>
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>

View file

@ -146,6 +146,10 @@ export class ServicesApiServices {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged });
}
accelerateWithCardOnFile$(txInput: string, token: string, verificationToken: string, referenceId: string, userApprovedUSD: number, userChallenged: boolean) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cardOnFile`, { txInput: txInput, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged });
}
getAccelerations$(): Observable<Acceleration[]> {
return this.httpClient.get<Acceleration[]>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations`);
}

View file

@ -7,7 +7,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft,
faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck,
faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline,
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes } from '@fortawesome/free-solid-svg-icons';
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MenuComponent } from '@components/menu/menu.component';
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
@ -463,5 +463,6 @@ export class SharedModule {
library.addIcons(faMoneyBillTrendUp);
library.addIcons(faRobot);
library.addIcons(faShareNodes);
library.addIcons(faCreditCard);
}
}