From 9b50b4961995681366581c8e280ccc196621e15c Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Thu, 25 Jan 2024 22:27:26 -0500 Subject: [PATCH] Implemented single nft bulk buy --- src/app/@api/order.api.ts | 5 + .../@shell/ui/header/header.component.html | 1 + src/app/@shell/ui/header/header.component.ts | 37 ++- .../nft-checkout/nft-checkout.component.html | 288 ++++++++++++++++-- .../nft-checkout/nft-checkout.component.ts | 82 +++-- src/app/pages/award/pages/new/new.page.ts | 4 +- src/app/pages/nft/pages/nft/nft.page.html | 85 ++++-- src/app/pages/nft/pages/nft/nft.page.ts | 31 ++ src/app/pages/nft/services/helper.service.ts | 12 + 9 files changed, 467 insertions(+), 78 deletions(-) diff --git a/src/app/@api/order.api.ts b/src/app/@api/order.api.ts index 4ddae44..71784d7 100644 --- a/src/app/@api/order.api.ts +++ b/src/app/@api/order.api.ts @@ -7,6 +7,7 @@ import { WEN_FUNC, Build5Request, NftPurchaseRequest, + NftPurchaseBulkRequest, OrderTokenRequest, AddressValidationRequest, NftBidRequest, @@ -27,6 +28,10 @@ export class OrderApi extends BaseApi { public orderNft = (req: Build5Request): Observable => this.request(WEN_FUNC.orderNft, req); + public orderNfts = ( + req: Build5Request, + ): Observable => this.request(WEN_FUNC.orderNftBulk, req); + public orderToken = ( req: Build5Request, ): Observable => this.request(WEN_FUNC.orderToken, req); diff --git a/src/app/@shell/ui/header/header.component.html b/src/app/@shell/ui/header/header.component.html index 31c4476..7f983d4 100644 --- a/src/app/@shell/ui/header/header.component.html +++ b/src/app/@shell/ui/header/header.component.html @@ -251,6 +251,7 @@ [isOpen]="isCheckoutOpen" [nft]="currentCheckoutNft" [collection]="currentCheckoutCollection" + [nftQuantity]="nftQty ?? 1" (wenOnClose)="closeCheckout()" > diff --git a/src/app/@shell/ui/header/header.component.ts b/src/app/@shell/ui/header/header.component.ts index b3844b8..7719c67 100644 --- a/src/app/@shell/ui/header/header.component.ts +++ b/src/app/@shell/ui/header/header.component.ts @@ -28,6 +28,7 @@ import { ROUTER_UTILS } from '@core/utils/router.utils'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { Collection, + CollectionType, FILE_SIZES, Member, Nft, @@ -78,6 +79,7 @@ export class HeaderComponent implements OnInit, OnDestroy { public isCheckoutOpen = false; public currentCheckoutNft?: Nft; public currentCheckoutCollection?: Collection; + public nftQty?: number; public notifications$: BehaviorSubject = new BehaviorSubject([]); private notificationRef?: NzNotificationRef; public expiryTicker$: BehaviorSubject = @@ -221,26 +223,47 @@ export class HeaderComponent implements OnInit, OnDestroy { } public async onOpenCheckout(): Promise { + // console.log('Open Checkout clicked'); const t = this.transaction$.getValue(); - if (!t?.payload.nft || !t.payload.collection) { - return; + let colId = ''; + let nftId = ''; + // console.log('open checkout transaction value: ', t); + + if (t?.payload.nftOrders && t?.payload.nftOrders?.length > 0) { + // console.log('open checkout passed if test for bulk order. bool (t?.payload.nftOrders) and t?.payload.nftOrders?.length (bulk count): ', t?.payload.nftOrders, t?.payload.nftOrders?.length); + colId = t?.payload.nftOrders[0].collection; + nftId = t?.payload.nftOrders[0].nft; + this.nftQty = t?.payload.nftOrders.length; + } else { + // console.log('open checkout failed if test for bulk order and will use nft, collection (colId, nftId): ', t?.payload.collection, t?.payload?.nft); + if (!t?.payload.nft || !t.payload.collection) { + return; + } + colId = t?.payload.collection; + nftId = t?.payload?.nft; + this.nftQty = 1; } + const collection: Collection | undefined = await firstValueFrom( - this.collectionApi.listen(t?.payload.collection), + this.collectionApi.listen(colId), ); let nft: Nft | undefined = undefined; - try { - nft = await firstValueFrom(this.nftApi.listen(t?.payload?.nft)); - } catch (_e) { - // If it's not classic or re-sale we're using placeholder NFT + nft = await firstValueFrom(this.nftApi.listen(nftId)); + // console.log('open checkout collection and nft value set (colId, collection, nftId, nft): ', colId, collection, nftId, nft); + + if (!nft) { + // console.log('open checkout try nft failed, previous nft value (nftId, nft): ', nftId, nft); if (collection?.placeholderNft) { nft = await firstValueFrom(this.nftApi.listen(collection?.placeholderNft)); + // console.log('open checkout try nft failed, will attempt to set nft based on collection placeholer (collection?.placeholderNft): ', collection?.placeholderNft); } } + if (nft && collection) { this.currentCheckoutCollection = collection; this.currentCheckoutNft = nft; this.isCheckoutOpen = true; + // console.log('Checkout Open initiated with the following values (collection, nft, bulk order bool, bulk order count, transaction', collection, nft, (t?.payload.nftOrders && t?.payload.nftOrders.length > 0), t?.payload.nftOrders?.length, t) this.cd.markForCheck(); } } diff --git a/src/app/components/nft/components/nft-checkout/nft-checkout.component.html b/src/app/components/nft/components/nft-checkout/nft-checkout.component.html index 5130502..1d87b99 100644 --- a/src/app/components/nft/components/nft-checkout/nft-checkout.component.html +++ b/src/app/components/nft/components/nft-checkout/nft-checkout.component.html @@ -65,41 +65,270 @@

{{ getTitle() }}

-
- Total price + +
+ You have selected to perform a bulk order of this NFT. Please review quantity + selected before finalizing and purchase.
-
-
- {{ targetPrice | formatToken : collection?.mintingData?.network : true | async }} +
+ +
+ + +
+ Total price
-
- {{ - calc(targetPrice, discount()) - | formatToken : collection?.mintingData?.network : true - | async - }} + +
+
+ {{ targetPrice | formatToken : collection?.mintingData?.network : true | async }} +
+
+ {{ + calc(targetPrice, discount()) + | formatToken : collection?.mintingData?.network : true + | async + }} +
-
-
+
+ {{ + currentStep !== stepType.CONFIRM + ? (targetAmount | formatToken : collection?.mintingData?.network : true | async) + : (targetPrice | formatToken : collection?.mintingData?.network : true | async) + }} +
+
+
+ + + -
- {{ - currentStep !== stepType.CONFIRM - ? (targetAmount | formatToken : collection?.mintingData?.network : true | async) - : (targetPrice | formatToken : collection?.mintingData?.network : true | async) - }} +
+ +
+
+ Price each +
+ +
+
+ {{ + targetPrice | formatToken : collection?.mintingData?.network : true | async + }} +
+
+ {{ + calc(targetPrice, discount()) + | formatToken : collection?.mintingData?.network : true + | async + }} +
+
+ +
+
+ {{ + currentStep !== stepType.CONFIRM + ? (pricePerItem + | formatToken : collection?.mintingData?.network : true + | async) + : (targetPrice + | formatToken : collection?.mintingData?.network : true + | async) + }} +
+
+
+ + +
+
+ Order quantity +
+
+
+ {{ nftQuantity }} +
+
+
+ + +
+
+ Total price +
+ +
+
+ {{ + targetPrice * nftQuantity + | formatToken : collection?.mintingData?.network : true + | async + }} +
+
+ {{ + calc(targetPrice, discount()) * nftQuantity + | formatToken : collection?.mintingData?.network : true + | async + }} +
+
+ +
+
+ {{ + currentStep !== stepType.CONFIRM && targetAmount !== null + ? (targetAmount + | formatToken : collection?.mintingData?.network : true + | async) + : (targetPrice * nftQuantity + | formatToken : collection?.mintingData?.network : true + | async) + }} +
+
+
-
+
+ + + +
+
+ +
+ Warning: Missing or Invalid Data +
+
+ +
+
+
+
+ Target Price +
+
+ {{ targetPrice }} +
+
+
+ +
+
+
+ Target Amount +
+
+ {{ targetAmount }} +
+
+
+ +
+
+
+ Quantity Selected +
+
+ {{ nftQuantity }} +
+
+
+ +
+
+
+ Discount +
+
+ {{ discount() }} +
+
+
+ +
+
+
+ Purchase Workflow Step +
+
+ {{ currentStep }} +
+
+
+
+
+
@@ -287,6 +516,7 @@

{{ getTitle() }}

(click)="goToNft()" nzSize="large" class="w-full mb-6 lg:mb-0 lg:mt-1" + *ngIf="!nftQuantity || nftQuantity <= 1" i18n > Show my NFT diff --git a/src/app/components/nft/components/nft-checkout/nft-checkout.component.ts b/src/app/components/nft/components/nft-checkout/nft-checkout.component.ts index ac136ea..a0aec08 100644 --- a/src/app/components/nft/components/nft-checkout/nft-checkout.component.ts +++ b/src/app/components/nft/components/nft-checkout/nft-checkout.component.ts @@ -34,6 +34,7 @@ import { Transaction, TransactionType, TRANSACTION_AUTO_EXPIRY_MS, + NftPurchaseRequest, } from '@build-5/interfaces'; import dayjs from 'dayjs'; import { BehaviorSubject, firstValueFrom, interval, Subscription, take } from 'rxjs'; @@ -64,6 +65,7 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { @Input() currentStep = StepType.CONFIRM; @Input() set isOpen(value: boolean) { + // console.log('Is Open changed:', value); this._isOpen = value; this.checkoutService.modalOpen$.next(value); } @@ -86,6 +88,7 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { if (this.currentStep === StepType.CONFIRM) { this.targetPrice = this._nft.availablePrice || this._nft.price || 0; + // console.log('targetPrice set, _nft.availablePrice, _nft.price, targetPrice: ', this._nft.availablePrice, this._nft.price, this.targetPrice); } } } @@ -112,6 +115,8 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { return this._collection; } + @Input() nftQuantity = 1; + @Output() wenOnClose = new EventEmitter(); public purchasedNft?: Nft | null; @@ -156,12 +161,14 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { ) {} public ngOnInit(): void { + // console.log('[nft-checkout] loaded, qty passed in: ', this.nftQuantity); this.receivedTransactions = false; const listeningToTransaction: string[] = []; this.transaction$.pipe(untilDestroyed(this)).subscribe((val) => { if (val && val.type === TransactionType.ORDER) { this.targetAddress = val.payload.targetAddress; this.targetAmount = val.payload.amount; + // console.log('target amount set using val.payload.amount. val: ', val); const expiresOn: dayjs.Dayjs = dayjs(val.payload.expiresOn!.toDate()); if (expiresOn.isBefore(dayjs()) || val.payload?.void || val.payload?.reconciled) { // It's expired. @@ -414,34 +421,69 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { return this.purchasedNft || this.nft; } + get pricePerItem(): number { + return (this.targetAmount ?? 0) / (this.nftQuantity || 1); + } + public async proceedWithOrder(): Promise { if (!this.collection || !this.nft || !this.agreeTermsConditions) { return; } - const params: any = { - collection: this.collection.uid, - }; + if (this.nftQuantity > 1) { + const nfts: NftPurchaseRequest[] = []; - if (this.collection.type === CollectionType.CLASSIC) { - params.nft = this.nft.uid; - } + for (let i = 0; i < this.nftQuantity; i++) { + const nftData: NftPurchaseRequest = { + collection: this.collection.uid, + }; - // If owner is set CollectionType is not relevant. - if (this.nft.owner) { - params.nft = this.nft.uid; - } + nfts.push(nftData); + } - await this.auth.sign(params, (sc, finish) => { - this.notification - .processRequest(this.orderApi.orderNft(sc), $localize`Order created.`, finish) - .subscribe((val: any) => { - this.transSubscription?.unsubscribe(); - setItem(StorageItem.CheckoutTransaction, val.uid); - this.transSubscription = this.orderApi.listen(val.uid).subscribe(this.transaction$); - this.pushToHistory(val, val.uid, dayjs(), $localize`Waiting for transaction...`); - }); - }); + const bulkPurchaseRequest = { + orders: nfts, + }; + + await this.auth.sign(bulkPurchaseRequest, (sc, finish) => { + this.notification + .processRequest(this.orderApi.orderNfts(sc), $localize`Order created.`, finish) + .subscribe((val: any) => { + this.transSubscription?.unsubscribe(); + setItem(StorageItem.CheckoutTransaction, val.uid); + this.transSubscription = this.orderApi + .listen(val.uid) + .subscribe(this.transaction$); + this.pushToHistory(val, val.uid, dayjs(), $localize`Waiting for transaction...`); + }); + }); + } else { + const params: any = { + collection: this.collection.uid, + }; + + if (this.collection.type === CollectionType.CLASSIC) { + params.nft = this.nft.uid; + } + + // If owner is set CollectionType is not relevant. + if (this.nft.owner) { + params.nft = this.nft.uid; + } + + await this.auth.sign(params, (sc, finish) => { + this.notification + .processRequest(this.orderApi.orderNft(sc), $localize`Order created.`, finish) + .subscribe((val: any) => { + this.transSubscription?.unsubscribe(); + setItem(StorageItem.CheckoutTransaction, val.uid); + this.transSubscription = this.orderApi + .listen(val.uid) + .subscribe(this.transaction$); + this.pushToHistory(val, val.uid, dayjs(), $localize`Waiting for transaction...`); + }); + }); + } } public getTitle(): any { diff --git a/src/app/pages/award/pages/new/new.page.ts b/src/app/pages/award/pages/new/new.page.ts index c8a5f8d..700132b 100644 --- a/src/app/pages/award/pages/new/new.page.ts +++ b/src/app/pages/award/pages/new/new.page.ts @@ -17,6 +17,8 @@ import { TEST_AVAILABLE_MINTABLE_NETWORKS, Token, TokenStatus, + getDefDecimalIfNotSet, + Network, } from '@build-5/interfaces'; import { BehaviorSubject, of, Subscription, switchMap } from 'rxjs'; import { filter, map } from 'rxjs/operators'; @@ -27,7 +29,7 @@ import { NotificationService } from './../../../../@core/services/notification/n import { AuthService } from './../../../../components/auth/services/auth.service'; import { FileApi } from '@api/file.api'; -import { getDefDecimalIfNotSet, Network } from '@build-5/interfaces'; + import { NzNotificationService } from 'ng-zorro-antd/notification'; import { NzUploadChangeParam, NzUploadFile, NzUploadXHRArgs } from 'ng-zorro-antd/upload'; diff --git a/src/app/pages/nft/pages/nft/nft.page.html b/src/app/pages/nft/pages/nft/nft.page.html index 2c6a472..2377797 100644 --- a/src/app/pages/nft/pages/nft/nft.page.html +++ b/src/app/pages/nft/pages/nft/nft.page.html @@ -248,28 +248,70 @@

{{ getTitle(data.nft$ | async) }}

>
-
- + + +
+ +
+ + + + + + + +
- -
+ + + + + + + +
+ +
@@ -775,6 +817,7 @@

[isOpen]="isCheckoutOpen" [nft]="data.nft$ | async" [collection]="data.collection$ | async" + [nftQuantity]="nftQtySelected" (wenOnClose)="isCheckoutOpen = false" > maxQuantity) { + this.nftQtySelected = maxQuantity; + this.resetInput(); + } else { + this.nftQtySelected = parsedQuantity; + } + } + + private resetInput() { + if (this.quantityInput) { + this.quantityInput.reset(this.nftQtySelected); + } + } + public discount(collection?: Collection | null, nft?: Nft | null): number { if (!collection?.space || !this.auth.member$.value || nft?.owner) { return 1; diff --git a/src/app/pages/nft/services/helper.service.ts b/src/app/pages/nft/services/helper.service.ts index ae5f98a..549f02f 100644 --- a/src/app/pages/nft/services/helper.service.ts +++ b/src/app/pages/nft/services/helper.service.ts @@ -179,6 +179,18 @@ export class HelperService { ); } + public getAvailNftQty(nft?: Nft | null, col?: Collection | null): number { + const isAvailableForSale = this.isAvailableForSale(nft, col); + + if (nft?.placeholderNft && isAvailableForSale) { + return col?.availableNfts || 0; + } else if (isAvailableForSale) { + return 1; + } + + return 0; + } + public canBeSetForSale(nft?: Nft | null): boolean { if (nft?.auctionFrom || nft?.availableFrom) { return false;