2024-04-14 16:29:56 +09:00
import { Component , OnInit , OnDestroy , Output , EventEmitter , Input , ChangeDetectorRef , SimpleChanges } from '@angular/core' ;
2024-04-13 20:53:19 +09:00
import { Subscription , tap , of , catchError } from 'rxjs' ;
import { ServicesApiServices } from '../../services/services-api.service' ;
import { nextRoundNumber } from '../../shared/common.utils' ;
import { StateService } from '../../services/state.service' ;
2024-04-13 23:07:19 +09:00
import { AudioService } from '../../services/audio.service' ;
2024-04-13 16:11:49 +09:00
@Component ( {
selector : 'app-accelerate-checkout' ,
templateUrl : './accelerate-checkout.component.html' ,
styleUrls : [ './accelerate-checkout.component.scss' ]
} )
export class AccelerateCheckout implements OnInit , OnDestroy {
2024-04-16 21:09:30 +09:00
@Input ( ) eta : number | null = null ;
2024-04-13 20:53:19 +09:00
@Input ( ) txid : string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad' ;
2024-04-14 16:29:56 +09:00
@Input ( ) scrollEvent : boolean ;
2024-04-13 23:07:19 +09:00
@Output ( ) close = new EventEmitter < null > ( ) ;
2024-04-13 20:53:19 +09:00
calculating = true ;
choosenOption : 'wait' | 'accelerate' = 'wait' ;
error = '' ;
// accelerator stuff
square : { appId : string , locationId : string } ;
accelerationUUID : string ;
estimateSubscription : Subscription ;
2024-04-13 23:07:19 +09:00
maxBidBoost : number ; // sats
2024-04-13 20:53:19 +09:00
cost : number ; // sats
// square
2024-04-13 23:07:19 +09:00
loadingCashapp = false ;
2024-04-13 20:53:19 +09:00
cashappSubmit : any ;
payments : any ;
cashAppPay : any ;
cashAppSubscription : Subscription ;
conversionsSubscription : Subscription ;
2024-04-14 16:29:56 +09:00
step : 'cta' | 'checkout' | 'processing' = 'cta' ;
2024-04-13 20:53:19 +09:00
constructor (
private servicesApiService : ServicesApiServices ,
2024-04-13 23:07:19 +09:00
private stateService : StateService ,
private audioService : AudioService ,
private cd : ChangeDetectorRef
) {
this . accelerationUUID = window . crypto . randomUUID ( ) ;
}
2024-04-13 16:11:49 +09:00
ngOnInit() {
2024-04-13 20:53:19 +09:00
const urlParams = new URLSearchParams ( window . location . search ) ;
if ( urlParams . get ( 'cash_request_id' ) ) { // Redirected from cashapp
2024-04-13 23:07:19 +09:00
this . insertSquare ( ) ;
this . setupSquare ( ) ;
this . step = 'processing' ;
2024-04-13 20:53:19 +09:00
}
2024-04-13 23:07:19 +09:00
this . servicesApiService . setupSquare $ ( ) . subscribe ( ids = > {
this . square = {
appId : ids.squareAppId ,
locationId : ids.squareLocationId
} ;
if ( this . step === 'cta' ) {
this . estimate ( ) ;
}
} ) ;
2024-04-13 16:11:49 +09:00
}
ngOnDestroy() {
2024-04-13 20:53:19 +09:00
if ( this . estimateSubscription ) {
this . estimateSubscription . unsubscribe ( ) ;
}
}
2024-04-14 16:29:56 +09:00
ngOnChanges ( changes : SimpleChanges ) : void {
if ( changes . scrollEvent ) {
this . scrollToPreview ( 'acceleratePreviewAnchor' , 'start' ) ;
}
}
2024-04-13 20:53:19 +09:00
/ * *
2024-04-14 16:29:56 +09:00
* Scroll to element id with or without setTimeout
* /
scrollToPreviewWithTimeout ( id : string , position : ScrollLogicalPosition ) {
setTimeout ( ( ) = > {
this . scrollToPreview ( id , position ) ;
} , 1000 ) ;
}
scrollToPreview ( id : string , position : ScrollLogicalPosition ) {
const acceleratePreviewAnchor = document . getElementById ( id ) ;
if ( acceleratePreviewAnchor ) {
this . cd . markForCheck ( ) ;
acceleratePreviewAnchor . scrollIntoView ( {
behavior : 'smooth' ,
inline : position ,
block : position ,
} ) ;
}
}
2024-04-16 16:56:37 +09:00
/ * *
2024-04-13 20:53:19 +09:00
* Accelerator
* /
estimate() {
if ( this . estimateSubscription ) {
this . estimateSubscription . unsubscribe ( ) ;
}
this . calculating = true ;
this . estimateSubscription = this . servicesApiService . estimate $ ( this . txid ) . pipe (
tap ( ( response ) = > {
this . calculating = false ;
if ( response . status === 204 ) {
this . error = ` cannot_accelerate_tx ` ;
} else {
const estimation = response . body ;
if ( ! estimation ) {
this . error = ` cannot_accelerate_tx ` ;
return ;
}
// Make min extra fee at least 50% of the current tx fee
2024-04-13 23:07:19 +09:00
const minExtraBoost = nextRoundNumber ( Math . max ( estimation . cost * 2 , estimation . txSummary . effectiveFee ) ) ;
2024-04-22 08:08:03 +02:00
const DEFAULT_BID_RATIO = 1 ;
2024-04-13 23:07:19 +09:00
this . maxBidBoost = minExtraBoost * DEFAULT_BID_RATIO ;
2024-04-14 00:19:14 +09:00
this . cost = this . maxBidBoost + estimation . mempoolBaseFee + estimation . vsizeFee ;
2024-04-13 20:53:19 +09:00
}
} ) ,
catchError ( ( response ) = > {
this . error = ` cannot_accelerate_tx ` ;
return of ( null ) ;
} )
) . subscribe ( ) ;
}
/ * *
* Square
* /
insertSquare ( ) : void {
//@ts-ignore
if ( window . Square ) {
return ;
}
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js' ;
if ( document . location . hostname === 'mempool-staging.fmt.mempool.space' ||
document . location . hostname === 'mempool-staging.va1.mempool.space' ||
document . location . hostname === 'mempool-staging.fra.mempool.space' ||
document . location . hostname === 'mempool-staging.tk7.mempool.space' ||
document . location . hostname === 'mempool.space' ) {
statsUrl = 'https://web.squarecdn.com/v1/square.js' ;
}
( function ( ) {
const d = document , g = d . createElement ( 'script' ) , s = d . getElementsByTagName ( 'script' ) [ 0 ] ;
// @ts-ignore
g . type = 'text/javascript' ; g . src = statsUrl ; s . parentNode . insertBefore ( g , s ) ;
} ) ( ) ;
}
setupSquare() {
const init = ( ) = > {
this . initSquare ( ) ;
} ;
//@ts-ignore
if ( ! window . Square ) {
console . debug ( 'Square.js failed to load properly. Retrying in 1 second.' ) ;
setTimeout ( init , 1000 ) ;
} else {
init ( ) ;
}
}
async initSquare ( ) : Promise < void > {
try {
//@ts-ignore
this . payments = window . Square . payments ( this . square . appId , this . square . locationId )
await this . requestCashAppPayment ( ) ;
} catch ( e ) {
2024-04-16 16:56:37 +09:00
console . debug ( 'Error loading Square Payments' , e ) ;
2024-04-13 20:53:19 +09:00
return ;
}
}
async requestCashAppPayment() {
if ( this . cashAppSubscription ) {
this . cashAppSubscription . unsubscribe ( ) ;
}
if ( this . conversionsSubscription ) {
this . conversionsSubscription . unsubscribe ( ) ;
}
this . conversionsSubscription = this . stateService . conversions $ . subscribe (
async ( conversions ) = > {
if ( this . cashAppPay ) {
2024-04-16 16:56:37 +09:00
this . cashAppPay . destroy ( ) ;
2024-04-13 20:53:19 +09:00
}
const redirectHostname = document . location . hostname === 'localhost' ? ` http://localhost:4200 ` : ` https:// ${ document . location . hostname } ` ;
2024-04-13 23:07:19 +09:00
const costUSD = this . step === 'processing' ? 69.69 : ( this . cost / 100 _000_000 * conversions . USD ) ; // When we're redirected to this component, the payment data is already linked to the payment token, so does not matter what amonut we put in there, therefore it's 69.69
2024-04-13 20:53:19 +09:00
const paymentRequest = this . payments . paymentRequest ( {
countryCode : 'US' ,
currencyCode : 'USD' ,
total : {
amount : costUSD.toString ( ) ,
label : 'Total' ,
pending : true ,
productUrl : ` ${ redirectHostname } /tracker/ ${ this . txid } ` ,
} ,
button : { shape : 'semiround' , size : 'small' , theme : 'light' }
} ) ;
this . cashAppPay = await this . payments . cashAppPay ( paymentRequest , {
2024-04-13 23:07:19 +09:00
redirectURL : ` ${ redirectHostname } /tracker/ ${ this . txid } ` ,
2024-04-13 20:53:19 +09:00
referenceId : ` accelerator- ${ this . txid . substring ( 0 , 15 ) } - ${ Math . round ( new Date ( ) . getTime ( ) / 1000 ) } ` ,
button : { shape : 'semiround' , size : 'small' , theme : 'light' }
} ) ;
2024-04-13 23:07:19 +09:00
if ( this . step === 'checkout' ) {
await this . cashAppPay . attach ( ` #cash-app-pay ` , { theme : 'light' , size : 'small' , shape : 'semiround' } )
}
this . loadingCashapp = false ;
2024-04-13 20:53:19 +09:00
const that = this ;
this . cashAppPay . addEventListener ( 'ontokenization' , function ( event ) {
const { tokenResult , error } = event . detail ;
if ( error ) {
this . error = error ;
} else if ( tokenResult . status === 'OK' ) {
that . servicesApiService . accelerateWithCashApp $ (
that . txid ,
tokenResult . token ,
tokenResult . details . cashAppPay . cashtag ,
tokenResult . details . cashAppPay . referenceId ,
that . accelerationUUID
) . subscribe ( {
next : ( ) = > {
2024-04-13 23:07:19 +09:00
that . audioService . playSound ( 'ascend-chime-cartoon' ) ;
2024-04-16 16:56:37 +09:00
if ( that . cashAppPay ) {
that . cashAppPay . destroy ( ) ;
}
setTimeout ( ( ) = > {
that . closeModal ( ) ;
2024-04-16 17:02:10 +09:00
if ( window . history . replaceState ) {
const urlParams = new URLSearchParams ( window . location . search ) ;
window . history . replaceState ( null , null , window . location . toString ( ) . replace ( ` ?cash_request_id= ${ urlParams . get ( 'cash_request_id' ) } ` , '' ) ) ;
}
2024-04-16 16:56:37 +09:00
} , 1000 ) ;
2024-04-13 20:53:19 +09:00
} ,
error : ( response ) = > {
if ( response . status === 403 && response . error === 'not_available' ) {
that . error = 'waitlisted' ;
} else {
that . error = response . error ;
2024-04-16 16:56:37 +09:00
setTimeout ( ( ) = > {
2024-04-16 17:02:10 +09:00
// Reset everything by reloading the page :D, can be improved
2024-04-16 16:56:37 +09:00
const urlParams = new URLSearchParams ( window . location . search ) ;
window . location . assign ( window . location . toString ( ) . replace ( ` ?cash_request_id= ${ urlParams . get ( 'cash_request_id' ) } ` , ` ` ) ) ;
} , 3000 ) ;
2024-04-13 20:53:19 +09:00
}
}
} ) ;
}
} ) ;
}
) ;
}
/ * *
* UI events
* /
enableCheckoutPage() {
2024-04-13 23:07:19 +09:00
this . step = 'checkout' ;
this . loadingCashapp = true ;
2024-04-13 20:53:19 +09:00
this . insertSquare ( ) ;
this . setupSquare ( ) ;
}
selectedOptionChanged ( event ) {
this . choosenOption = event . target . id ;
}
2024-04-13 09:05:05 +00:00
closeModal ( ) : void {
this . close . emit ( ) ;
}
2024-04-13 16:11:49 +09:00
}