From d654ffafa9e2e0fa8b0668098f44eec3b4c54bfc Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Thu, 18 Jan 2024 18:26:20 -0500 Subject: [PATCH 01/24] Initial shopping cart and sweep to cart work --- .vscode/launch.json | 18 ++ src/app/@api/collection.api.ts | 4 + src/app/@api/nft.api.ts | 4 + .../filter-storage/filter-storage.service.ts | 1 + .../@core/services/router/router.service.ts | 9 +- src/app/@core/utils/local-storage.utils.ts | 1 + .../@shell/ui/header/header.component.html | 31 +++ src/app/@shell/ui/header/header.component.ts | 24 ++ src/app/@shell/ui/header/header.module.ts | 2 + .../algolia/services/algolia.service.ts | 14 +- src/app/components/cart/cart.module.ts | 39 ++++ .../cart-modal/cart-modal.component.html | 139 ++++++++++++ .../cart-modal/cart-modal.component.less | 0 .../cart-modal/cart-modal.component.ts | 207 ++++++++++++++++++ .../checkout/checkout-overlay.component.html | 7 + .../checkout/checkout-overlay.component.less | 0 .../checkout/checkout-overlay.component.ts | 14 ++ .../components/cart/services/cart.service.ts | 123 +++++++++++ .../components/icon/cart/cart.component.html | 11 + .../components/icon/cart/cart.component.less | 0 .../components/icon/cart/cart.component.ts | 9 + src/app/components/icon/icon.module.ts | 3 + .../nft-card/nft-card.component.html | 18 ++ .../components/nft-card/nft-card.component.ts | 17 ++ src/app/pages/collection/collection.module.ts | 2 + .../collection/nfts/collectionNfts.service.ts | 34 +++ .../pages/collection/nfts/nfts.page.html | 37 ++-- .../pages/collection/nfts/nfts.page.ts | 140 +++++++++++- .../pages/collections/collections.page.html | 33 +++ .../pages/collections/collections.page.ts | 1 + src/app/pages/nft/pages/nft/nft.page.html | 55 +++-- src/app/pages/nft/pages/nft/nft.page.ts | 24 ++ src/app/pages/nft/services/helper.service.ts | 32 ++- src/theme/02-components/modal.less | 9 + 34 files changed, 1012 insertions(+), 50 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/app/components/cart/cart.module.ts create mode 100644 src/app/components/cart/components/cart-modal/cart-modal.component.html create mode 100644 src/app/components/cart/components/cart-modal/cart-modal.component.less create mode 100644 src/app/components/cart/components/cart-modal/cart-modal.component.ts create mode 100644 src/app/components/cart/components/checkout/checkout-overlay.component.html create mode 100644 src/app/components/cart/components/checkout/checkout-overlay.component.less create mode 100644 src/app/components/cart/components/checkout/checkout-overlay.component.ts create mode 100644 src/app/components/cart/services/cart.service.ts create mode 100644 src/app/components/icon/cart/cart.component.html create mode 100644 src/app/components/icon/cart/cart.component.less create mode 100644 src/app/components/icon/cart/cart.component.ts create mode 100644 src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..78837d7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "NEW Launch npm start", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "start" + ], + "skipFiles": [ + "/**" + ], + "console": "integratedTerminal" + } + ] +} diff --git a/src/app/@api/collection.api.ts b/src/app/@api/collection.api.ts index ffec82b..25e15ac 100644 --- a/src/app/@api/collection.api.ts +++ b/src/app/@api/collection.api.ts @@ -34,6 +34,10 @@ export class CollectionApi extends BaseApi { super(Dataset.COLLECTION, httpClient); } + public getCollectionById(collectionId: string): Observable { + return this.listen(collectionId); + } + public mintCollection = ( req: Build5Request, ): Observable => this.request(WEN_FUNC.mintCollection, req); diff --git a/src/app/@api/nft.api.ts b/src/app/@api/nft.api.ts index ca4fbe8..2d0bf7c 100644 --- a/src/app/@api/nft.api.ts +++ b/src/app/@api/nft.api.ts @@ -62,6 +62,10 @@ export class NftApi extends BaseApi { public stakeNft = (req: Build5Request): Observable => this.request(WEN_FUNC.stakeNft, req); + public getNftById(nftId: string): Observable { + return this.listen(nftId); + } + public successfullOrders( nftId: string, network?: Network, diff --git a/src/app/@core/services/filter-storage/filter-storage.service.ts b/src/app/@core/services/filter-storage/filter-storage.service.ts index 8705908..b9a092c 100644 --- a/src/app/@core/services/filter-storage/filter-storage.service.ts +++ b/src/app/@core/services/filter-storage/filter-storage.service.ts @@ -51,6 +51,7 @@ export interface MarketCollectionsFilters { access?: string[]; space?: string[]; category?: string[]; + status?: string[]; }; range?: { price: string; diff --git a/src/app/@core/services/router/router.service.ts b/src/app/@core/services/router/router.service.ts index 529944a..dfdfb6f 100644 --- a/src/app/@core/services/router/router.service.ts +++ b/src/app/@core/services/router/router.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@angular/core'; -import { NavigationEnd, Router } from '@angular/router'; +import { NavigationEnd, Router, Event } from '@angular/router'; import { ROUTER_UTILS } from '@core/utils/router.utils'; import { BehaviorSubject } from 'rxjs'; import { DeviceService } from '../device'; +import { filter } from 'rxjs/operators'; @Injectable({ providedIn: 'root', @@ -21,6 +22,12 @@ export class RouterService { public urlToNewToken = '/' + ROUTER_UTILS.config.token.root + '/new'; constructor(private router: Router, private deviceService: DeviceService) { + //this.router.events.pipe( + // filter((event: Event): event is NavigationEnd => event instanceof NavigationEnd) + //).subscribe((event: NavigationEnd) => { + // console.log('Navigation Event:', event); + //}); + this.updateVariables(); this.router.events.subscribe((obj) => { diff --git a/src/app/@core/utils/local-storage.utils.ts b/src/app/@core/utils/local-storage.utils.ts index a70f86d..7f2722e 100644 --- a/src/app/@core/utils/local-storage.utils.ts +++ b/src/app/@core/utils/local-storage.utils.ts @@ -25,6 +25,7 @@ export enum StorageItem { SelectedTradePriceOption = 'App/selectedTradePriceOption', DepositNftTransaction = 'App/depositNftTransaction-', StakeNftTransaction = 'App/stakeNftTransaction-', + CartItems = 'App/cartItems', } export const getBitItemItem = (nftId: string): unknown | null => { diff --git a/src/app/@shell/ui/header/header.component.html b/src/app/@shell/ui/header/header.component.html index 31c4476..f84fa47 100644 --- a/src/app/@shell/ui/header/header.component.html +++ b/src/app/@shell/ui/header/header.component.html @@ -43,6 +43,19 @@ + + + + + +
 
+
 
+ + + + + + Your cart is empty. + + +
+ + +
+ + + diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.less b/src/app/components/cart/components/cart-modal/cart-modal.component.less new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.ts b/src/app/components/cart/components/cart-modal/cart-modal.component.ts new file mode 100644 index 0000000..adcc387 --- /dev/null +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.ts @@ -0,0 +1,207 @@ +// cart-modal.component.ts +import { + Component, + OnInit, + OnDestroy, + ChangeDetectorRef, + ChangeDetectionStrategy +} from '@angular/core'; +import { + Nft, + Collection, + MIN_AMOUNT_TO_TRANSFER, + } from '@build-5/interfaces'; +import { Subscription, forkJoin, map, take, catchError, of } from 'rxjs'; +import { CartService, CartItem } from './../../services/cart.service'; +import { AuthService } from '@components/auth/services/auth.service'; +//import { ActivatedRoute, Router } from '@angular/router'; +import { ROUTER_UTILS } from '@core/utils/router.utils'; +import { NzModalService } from 'ng-zorro-antd/modal'; +import { CheckoutOverlayComponent } from '../checkout/checkout-overlay.component'; +import { NftApi } from '@api/nft.api'; +//import { FormControl, FormGroup, Validators } from '@angular/forms'; + +@Component({ + selector: 'app-cart-modal', + templateUrl: './cart-modal.component.html', + styleUrls: ['./cart-modal.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CartModalComponent implements OnInit, OnDestroy { + private subscriptions = new Subscription(); + public collectionPath: string = ROUTER_UTILS.config.collection.root; + public nftPath: string = ROUTER_UTILS.config.nft.root; + public cartItemsQuantities: number[] = []; + cartItemPrices: { [key: string]: { originalPrice: number, discountedPrice: number } } = {}; + + constructor( + public cartService: CartService, + private cd: ChangeDetectorRef, + public auth: AuthService, + private modalService: NzModalService, + private nftApi: NftApi, + ) {} + + ngOnInit() { + this.subscriptions.add(this.cartService.getCartItems().subscribe(items => { + //console.log('[CartModalComponent-ngOnInit] Cart items updated:', items); + this.cartItemsStatus = items.map(item => this.cartItemStatus(item)); + this.cartItemsQuantities = items.map(item => this.cartItemSaleAvailableQty(item)); + items.forEach(item => { + const originalPrice = this.calcPrice(item, 1); + const discountedPrice = this.calcPrice(item, this.discount(item.collection, item.nft)); + this.cartItemPrices[item.nft.uid] = { originalPrice, discountedPrice }; + }); + })); + + this.subscriptions.add(this.cartService.showCart$.subscribe(show => { + if (show) { + this.refreshCartData(); + } + })); + } + + cartItemsStatus: string[] = []; + + trackByItemId(index: number, item: CartItem): string { + return item.nft.uid; + } + + public removeFromCart(itemId: string): void { + this.cartService.removeFromCart(itemId); + } + + private refreshCartData() { + //console.log('Refreshing cart items...'); + const cartItems = this.cartService.getCartItems().getValue(); + //console.log('Current cart items:', cartItems); + + const freshDataObservables = cartItems.map(item => + this.nftApi.getNftById(item.nft.uid).pipe( + take(1), + map(freshNft => { + //console.log(`Fetched fresh data for NFT ${item.nft.uid}:`, freshNft); + return freshNft ? { ...item, nft: freshNft } : item; + }), + catchError(error => { + //console.error(`Error fetching fresh data for NFT ${item.nft.uid}:`, error); + return of(item); // Return the original item in case of error + }) + ) + ); + + forkJoin(freshDataObservables).subscribe( + freshCartItems => { + //console.log('Fresh cart items:', freshCartItems); + + this.cartService.updateCartItems(freshCartItems); + + // Perform existing refresh operations + this.cartItemsStatus = freshCartItems.map(item => this.cartItemStatus(item)); + this.cartItemsQuantities = freshCartItems.map(item => this.cartItemSaleAvailableQty(item)); + freshCartItems.forEach(item => { + const originalPrice = this.calcPrice(item, 1); + const discountedPrice = this.calcPrice(item, this.discount(item.collection, item.nft)); + this.cartItemPrices[item.nft.uid] = { originalPrice, discountedPrice }; + }); + + //console.log('Finished refreshing cart items.'); + + this.cd.markForCheck(); + }, + error => { + console.error('Error while refreshing cart items:', error); + // Handle the error appropriately + // Maybe show a user-friendly message or perform some recovery logic + } + ); + } + + public updateQuantity(event: Event, itemId: string): void { + const inputElement = event.target as HTMLInputElement; + const newQuantity = Number(inputElement.value); + + if (newQuantity === 0) { + this.cartService.removeFromCart(itemId); + } else { + const cartItems = this.cartService.getCartItems().getValue(); + const itemIndex = cartItems.findIndex(cartItem => cartItem.nft.uid === itemId); + if (itemIndex !== -1) { + cartItems[itemIndex].quantity = newQuantity; + this.cartService.saveCartItems(); + } + } + } + + public discount(collection?: Collection | null, nft?: Nft | null): number { + if (!collection?.space || !this.auth.member$.value || nft?.owner) { + return 1; + } + + const spaceRewards = (this.auth.member$.value.spaces || {})[collection.space]; + const descDiscounts = [...(collection.discounts || [])].sort((a, b) => b.amount - a.amount); + for (const discount of descDiscounts) { + const awardStat = (spaceRewards?.awardStat || {})[discount.tokenUid!]; + const memberTotalReward = awardStat?.totalReward || 0; + if (memberTotalReward >= discount.tokenReward) { + return 1 - discount.amount; + } + } + return 1; + } + + public calc(amount: number | null | undefined, discount: number): number { + let finalPrice = Math.ceil((amount || 0) * discount); + if (finalPrice < MIN_AMOUNT_TO_TRANSFER) { + finalPrice = MIN_AMOUNT_TO_TRANSFER; + } + + return finalPrice; + } + + private calcPrice(item: CartItem, discount: number): number { + const itemPrice = item.nft?.availablePrice || item.nft?.price || 0; + return this.calc(itemPrice, discount); // assuming calc method applies the discount + } + + public cartItemStatus(item: CartItem): any { + //console.log("[cart-modal.component-cartItemStatus] function called"); + const itemAvailable = this.cartService.isCartItemAvailableForSale(item); + if(itemAvailable) { + //console.log("[cart-modal.component-cartItemStatus] returning Available, itemAvailable: " + itemAvailable); + return "Available"; + }; + //console.log("[cart-modal.component-cartItemStatus] returning Not Available, itemAvailable: " + itemAvailable); + return "Not Available"; + }; + + private cartItemSaleAvailableQty(item: CartItem): number { + //console.log("[cart-modal.component-cartItemSaleAvailableQty] function called"); + const availQty = this.cartService.getAvailableNftQuantity(item); + //console.log("[cart-modal.component] cartItemSaleAvailableQty, qty: " + availQty); + return availQty; + } + + handleClose(): void { + this.cartService.hideCart(); + } + + public handleCheckout(): void { + const cartItems = this.cartService.getCartItems().getValue(); + //console.log('Proceeding to checkout with items:', cartItems); + + // Open the checkout overlay here + this.modalService.create({ + nzTitle: 'Checkout', + nzContent: CheckoutOverlayComponent, + nzComponentParams: { + items: cartItems + }, + nzFooter: null, + }); + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } +} diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.html b/src/app/components/cart/components/checkout/checkout-overlay.component.html new file mode 100644 index 0000000..7abd0d0 --- /dev/null +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.html @@ -0,0 +1,7 @@ + +
+ +
+ + + diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.less b/src/app/components/cart/components/checkout/checkout-overlay.component.less new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.ts b/src/app/components/cart/components/checkout/checkout-overlay.component.ts new file mode 100644 index 0000000..cf06037 --- /dev/null +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.ts @@ -0,0 +1,14 @@ +import { Component, Input } from '@angular/core'; +import { CartItem } from './../../services/cart.service'; + +@Component({ + selector: 'app-checkout-overlay', + templateUrl: './checkout-overlay.component.html', + styleUrls: ['./checkout-overlay.component.less'], +}) +export class CheckoutOverlayComponent { + @Input() items: CartItem[] = []; + // Implement your payment form logic here + + // ... other methods +} diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts new file mode 100644 index 0000000..009ec54 --- /dev/null +++ b/src/app/components/cart/services/cart.service.ts @@ -0,0 +1,123 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { + Nft, + Collection, +} from '@build-5/interfaces'; +import { getItem, setItem, StorageItem } from './../../../@core/utils/local-storage.utils'; +import { NzNotificationService } from 'ng-zorro-antd/notification'; +import { HelperService } from '@pages/nft/services/helper.service'; +import { AuthService } from '@components/auth/services/auth.service'; + +export interface CartItem { + nft: Nft; + collection: Collection; + quantity: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class CartService { + + private showCartSubject = new BehaviorSubject(false); + public showCart$ = this.showCartSubject.asObservable(); + private cartItemsSubject = new BehaviorSubject(this.loadCartItems()); + + constructor( + private notification: NzNotificationService, + private helperService: HelperService, + public auth: AuthService, + ) { + //console.log('CartService instance created'); + } + + public showCart(): void { + this.showCartSubject.next(true); + } + + public hideCart(): void { + this.showCartSubject.next(false); + } + + public refreshCartItems(): void { + this.cartItemsSubject.next(this.cartItemsSubject.value); + } + + public addToCart(cartItem: CartItem): void { + //console.log('[CartService] addToCart function called.'); + const currentItems = this.cartItemsSubject.value; + + const isItemAlreadyInCart = currentItems.some(item => item.nft.uid === cartItem.nft.uid); + + if (!isItemAlreadyInCart) { + const updatedCartItems = [...currentItems, cartItem]; + this.cartItemsSubject.next(updatedCartItems); + this.saveCartItems(); + this.notification.success($localize`NFT ` + cartItem.nft.name + ` from collection ` + cartItem.collection.name + ` has been added to your cart.`, ''); + //console.log('[CartService] NFT added to cart:', cartItem); + } else { + //console.log('[CartService] NFT is already in the cart:', cartItem); + this.notification.error($localize`This NFT already exists in your cart.`, ''); + } + } + + public removeFromCart(itemId: string): void { + //console.log('[CartService] removeFromCart function called.'); + const updatedCartItems = this.cartItemsSubject.value.filter(item => item.nft.uid !== itemId); + this.cartItemsSubject.next(updatedCartItems); + this.saveCartItems(); + //console.log('[CartService-removeFromCart] Cart updated:', updatedCartItems); + } + + public getCartItems(): BehaviorSubject { + //console.log('[CartService] getCartItems function called.'); + return this.cartItemsSubject; + } + + public updateCartItems(updatedItems: CartItem[]): void { + this.cartItemsSubject.next(updatedItems); // Update the BehaviorSubject with the new cart items + this.saveCartItems(); // Save the updated cart items to local storage or your backend + } + + public saveCartItems(): void { + //console.log('[CartService] getCartItems function called.'); + setItem(StorageItem.CartItems, this.cartItemsSubject.value); + //console.log('[CartService] Saving cart items to local storage:', this.cartItemsSubject.value); + } + + private loadCartItems(): CartItem[] { + //console.log('[CartService] Loading cart items from local storage'); + const items = getItem(StorageItem.CartItems) as CartItem[]; + //console.log('[CartService] Cart items loaded from local storage:', items); + return items || []; + } + + public isNftAvailableForSale(nft: Nft, collection: Collection): boolean { + const isLocked = this.helperService.isLocked(nft, collection, true); + const isOwner = nft.owner === this.auth.member$.value?.uid; + const availableForSale = this.helperService.isAvailableForSale(nft, collection); + //console.log(`[cart.service-isNftAvailableForSale] results for NFT ${nft.name}; availableForSale: ${availableForSale}, isLocked: ${isLocked}, isOwner: ${isOwner}`); + + return !isLocked && availableForSale && (!isOwner || !nft.owner); + } + + public isCartItemAvailableForSale(cartItem: CartItem): boolean { + return this.isNftAvailableForSale(cartItem.nft, cartItem.collection); + } + + public getAvailableNftQuantity(cartItem: CartItem): number { + const isAvailableForSale = this.helperService.isAvailableForSale(cartItem.nft, cartItem.collection); + //console.log("[cart.service-getAvailableNftQuantity] function called for cartItem.nft.name: " + cartItem.nft.name + ", isAvailableForSale: " + isAvailableForSale); + + if (cartItem.nft.placeholderNft && isAvailableForSale) { + //console.log("[service-getAvailableNftQuantity] returning qty, placeholder: " + cartItem.nft.placeholderNft + ". availableNfts " + cartItem.collection.availableNfts); + return cartItem.collection.availableNfts || 0; + } else if (isAvailableForSale) { + //console.log("[service-getAvailableNftQuantity] returning 1, isAvailableForSale: " + isAvailableForSale + ". placeholder: " + cartItem.nft.placeholderNft); + return 1; + } + //console.log("[service-getAvailableNftQuantity] returning 0, isAvailableForSale: " + isAvailableForSale + ". placeholder: " + cartItem.nft.placeholderNft); + return 0; + } +} diff --git a/src/app/components/icon/cart/cart.component.html b/src/app/components/icon/cart/cart.component.html new file mode 100644 index 0000000..4c29236 --- /dev/null +++ b/src/app/components/icon/cart/cart.component.html @@ -0,0 +1,11 @@ + + + + + diff --git a/src/app/components/icon/cart/cart.component.less b/src/app/components/icon/cart/cart.component.less new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/icon/cart/cart.component.ts b/src/app/components/icon/cart/cart.component.ts new file mode 100644 index 0000000..1886bfa --- /dev/null +++ b/src/app/components/icon/cart/cart.component.ts @@ -0,0 +1,9 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'wen-icon-cart', + templateUrl: './cart.component.html', + styleUrls: ['./cart.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CartIconComponent {} diff --git a/src/app/components/icon/icon.module.ts b/src/app/components/icon/icon.module.ts index dc57b80..4098511 100644 --- a/src/app/components/icon/icon.module.ts +++ b/src/app/components/icon/icon.module.ts @@ -12,6 +12,7 @@ import { AssemblyIconComponent } from './assembly/assembly.component'; import { AwardIconComponent } from './award/award.component'; import { BellIconComponent } from './bell/bell.component'; import { CalendarIconComponent } from './calendar/calendar.component'; +import { CartIconComponent } from './cart/cart.component'; import { CheckCircleIconComponent } from './check-circle/check-circle.component'; import { CheckIconComponent } from './check/check.component'; import { CloseIconComponent } from './close/close.component'; @@ -187,6 +188,7 @@ import { WalletIconComponent } from './wallet/wallet.component'; SpecificMembersOnlyIconComponent, SadCryIconComponent, LinkBrokenIconComponent, + CartIconComponent, ], exports: [ GlobeIconComponent, @@ -279,6 +281,7 @@ import { WalletIconComponent } from './wallet/wallet.component'; SpecificMembersOnlyIconComponent, SadCryIconComponent, LinkBrokenIconComponent, + CartIconComponent, ], imports: [CommonModule], }) diff --git a/src/app/components/nft/components/nft-card/nft-card.component.html b/src/app/components/nft/components/nft-card/nft-card.component.html index 49f610d..2e61b60 100644 --- a/src/app/components/nft/components/nft-card/nft-card.component.html +++ b/src/app/components/nft/components/nft-card/nft-card.component.html @@ -238,6 +238,24 @@ > + + + + + diff --git a/src/app/components/nft/components/nft-card/nft-card.component.ts b/src/app/components/nft/components/nft-card/nft-card.component.ts index 3c70eb3..f755463 100644 --- a/src/app/components/nft/components/nft-card/nft-card.component.ts +++ b/src/app/components/nft/components/nft-card/nft-card.component.ts @@ -10,6 +10,7 @@ import { UnitsService } from '@core/services/units'; import { ROUTER_UTILS } from '@core/utils/router.utils'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { HelperService } from '@pages/nft/services/helper.service'; +import { CartService } from '@components/cart/services/cart.service'; import { Access, Collection, @@ -89,6 +90,7 @@ export class NftCardComponent { private memberApi: MemberApi, private fileApi: FileApi, private cache: CacheService, + public cartService: CartService, ) {} public onBuy(event: MouseEvent): void { @@ -182,4 +184,19 @@ export class NftCardComponent { public get collectionStatuses(): typeof CollectionStatus { return CollectionStatus; } + + public addToCart(event: MouseEvent, nft: Nft | null | undefined, collection: Collection | null | undefined): void { + event.stopPropagation(); + event.preventDefault(); + + if (nft && collection) { + console.log('[NftCardComponent] Adding item to cart:', nft, collection); + this.cartService.addToCart({ nft, collection, quantity: 1 }); + // Optionally, provide feedback to the user + } else { + // Handle the case when nft or collection is null or undefined + console.error('Attempted to add a null or undefined NFT or Collection to the cart'); + } + } + } diff --git a/src/app/pages/collection/collection.module.ts b/src/app/pages/collection/collection.module.ts index c79cc5b..ce5f5ba 100644 --- a/src/app/pages/collection/collection.module.ts +++ b/src/app/pages/collection/collection.module.ts @@ -47,6 +47,7 @@ import { CollectionPage } from './pages/collection/collection.page'; import { CollectionNFTsPage } from './pages/collection/nfts/nfts.page'; import { UpsertPage } from './pages/upsert/upsert.page'; import { DataService } from './services/data.service'; +import { NzSliderModule } from 'ng-zorro-antd/slider'; @NgModule({ declarations: [CollectionPage, UpsertPage, CollectionAboutComponent, CollectionNFTsPage], @@ -97,6 +98,7 @@ import { DataService } from './services/data.service'; IpfsBadgeModule, IotaInputModule, CollectionMintNetworkModule, + NzSliderModule, ], providers: [DataService, FilterService], }) diff --git a/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts b/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts new file mode 100644 index 0000000..6292e0c --- /dev/null +++ b/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { Nft, Collection } from '@build-5/interfaces'; +import { CartService } from '@components/cart/services/cart.service'; + +@Injectable({ + providedIn: 'root' +}) +export class CollectionNftStateService { + private listedNftsSubject = new BehaviorSubject([]); + public listedNfts$ = this.listedNftsSubject.asObservable(); + private availableNftsCountSubject = new BehaviorSubject(0); + public availableNftsCount$ = this.availableNftsCountSubject.asObservable(); + + constructor( + private cartService: CartService, + ) { } + + public setListedNfts(nfts: Nft[], collection: Collection) { + this.listedNftsSubject.next(nfts); + this.updateAvailableNftsCount(nfts, collection); + } + + private updateAvailableNftsCount(nfts: Nft[], collection: Collection) { + const availableNftsCount = nfts.filter(nft => this.cartService.isNftAvailableForSale(nft, collection)).length; + this.availableNftsCountSubject.next(availableNftsCount); + } + + public getListedNfts(): Nft[] { + return this.listedNftsSubject.getValue(); + } +} + + diff --git a/src/app/pages/collection/pages/collection/nfts/nfts.page.html b/src/app/pages/collection/pages/collection/nfts/nfts.page.html index 3ce717a..4a173b8 100644 --- a/src/app/pages/collection/pages/collection/nfts/nfts.page.html +++ b/src/app/pages/collection/pages/collection/nfts/nfts.page.html @@ -4,18 +4,27 @@ -
- - - - {{ state.nbHits | number }} records - - - + +
+
+ + +
+ + {{ state.nbHits | number }} records + +
+ +
+ {{ sweepCount }} + +
+
+
+
+ -
+
+
(); + availableNftsCount = 0; + collection: Collection | null = null; + constructor( public filter: FilterService, public deviceService: DeviceService, public nftApi: NftApi, + private collectionApi: CollectionApi, public cd: ChangeDetectorRef, public filterStorageService: FilterStorageService, public cacheService: CacheService, public readonly algoliaService: AlgoliaService, - ) {} + public cartService: CartService, + private notification: NzNotificationService, + private collectionNftStateService: CollectionNftStateService, + ) { } public ngOnInit(): void { + if (this.collectionId) { + this.collectionApi.getCollectionById(this.collectionId) + .pipe(take(1)) + .subscribe({ + next: (collectionData) => { + if (collectionData) { + this.collection = collectionData; + this.collectionNftStateService.setListedNfts(this.originalNfts, this.collection); + } + }, + error: err => { + console.error('Error fetching collection:', err); + this.notification.error($localize`Error occurred while fetching collection.`, ''); + } + }); + } + + this.collectionNftStateService.availableNftsCount$ + .pipe(takeUntil(this.destroy$)) + .subscribe(count => { + this.availableNftsCount = count; + }); + + setTimeout(() => { + this.sweepCount = 1; // or whatever the initial value should be + this.cd.markForCheck(); // trigger change detection manually + }, 0); + + console.log('Component initialized, sweepCount:', this.sweepCount); + // Algolia change detection bug fix setInterval(() => this.cd.markForCheck(), 500); } + ngAfterViewInit() { + console.log('View fully initialized, slider should now be rendered, sweepCount: ', this.sweepCount); + this.cd.detectChanges(); + this.cd.markForCheck(); + } + public ngOnChanges(): void { // TODO comeup with better process. setTimeout(() => { @@ -91,16 +145,31 @@ export class CollectionNFTsPage implements OnInit, OnChanges { }, 500); } + public captureOriginalHits(hits: any[]) { + if (hits && hits.length > 0 && this.collection) { + this.originalNfts = hits; + this.collectionNftStateService.setListedNfts(hits, this.collection); + console.log('Original hits captured:', this.originalNfts); + } else { + console.log('Received empty hits array or collection is not available, ignoring to preserve existing listedNfts.'); + } + } + public trackByUid(_index: number, item: any): number { return item.uid; } - public convertAllToSoonaverseModel(algoliaItems: any[]) { - return algoliaItems.map((algolia) => ({ + public convertAllToSoonaverseModel = (algoliaItems: any[]) => { + // Capture the original hits + this.captureOriginalHits(algoliaItems); + + // Proceed with the transformation + const transformedItems = algoliaItems.map((algolia) => ({ ...algolia, availableFrom: Timestamp.fromMillis(+algolia.availableFrom), })); - } + return transformedItems; + }; public get collapseTypes(): typeof CollapseType { return CollapseType; @@ -109,4 +178,63 @@ export class CollectionNFTsPage implements OnInit, OnChanges { public get algoliaCheckboxFilterTypes(): typeof AlgoliaCheckboxFilterType { return AlgoliaCheckboxFilterType; } + + public sweepToCart(count: number) { + if (!this.collectionId) { + this.notification.error($localize`Collection ID is not available.`, ''); + return; + } + + this.collectionApi.getCollectionById(this.collectionId) + .pipe( + take(1), + filter((collection): collection is Collection => collection !== undefined), + switchMap((collection: Collection) => { + const listedNfts = this.collectionNftStateService.getListedNfts(); + console.log('[sweep] Listed NFTs:', listedNfts); + + // Filter NFTs based on their availability for sale + const nftsForSale = listedNfts.filter(nft => + this.cartService.isNftAvailableForSale(nft, collection) + ); + console.log('[sweep] NFTs for sale:', nftsForSale); + + // Sort the NFTs by price and take the top 'count' NFTs + const nftsToAdd = nftsForSale + .slice(0, Math.min(count, 20)) + .sort((a, b) => { + const priceA = a.availablePrice != null ? a.availablePrice : 0; + const priceB = b.availablePrice != null ? b.availablePrice : 0; + return priceA - priceB; + }); + + // Add the selected NFTs to the cart + nftsToAdd.forEach(nft => { + const cartItem = { nft, collection, quantity: 1 }; + this.cartService.addToCart(cartItem); + }); + + console.log('[sweep] NFTs added to cart:', nftsToAdd); + this.notification.success($localize`NFTs swept into your cart, open cart to review added items.`, ''); + + return nftsToAdd; // You can return something relevant here if needed + }), + takeUntil(this.destroy$) + ) + .subscribe({ + next: nftsToAdd => { + // Handle successful addition + console.log('[sweep] NFTs successfully added to cart:', nftsToAdd); + }, + error: err => { + console.error('[sweep] Error fetching collection:', err); + this.notification.error($localize`Error occurred while fetching collection.`, ''); + } + }); + } + + public ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } } diff --git a/src/app/pages/market/pages/collections/collections.page.html b/src/app/pages/market/pages/collections/collections.page.html index a8fa766..1a9b87b 100644 --- a/src/app/pages/market/pages/collections/collections.page.html +++ b/src/app/pages/market/pages/collections/collections.page.html @@ -130,6 +130,7 @@ sortBy: filterStorageService.marketCollectionsFilters$.value.sortBy, range: filterStorageService.marketCollectionsFilters$.value.range, refinementList: { + status: filterStorageService.marketCollectionsFilters$.value.refinementList?.status, category: filterStorageService.marketCollectionsFilters$.value.refinementList?.category, space: filterStorageService.marketCollectionsFilters$.value.refinementList?.space, access: $event @@ -163,6 +164,7 @@ range: filterStorageService.marketCollectionsFilters$.value.range, refinementList: { access: filterStorageService.marketCollectionsFilters$.value.refinementList?.access, + status: filterStorageService.marketCollectionsFilters$.value.refinementList?.status, category: filterStorageService.marketCollectionsFilters$.value.refinementList?.category, space: $event } @@ -193,6 +195,7 @@ refinementList: { access: filterStorageService.marketCollectionsFilters$.value.refinementList?.access, space: filterStorageService.marketCollectionsFilters$.value.refinementList?.space, + status: filterStorageService.marketCollectionsFilters$.value.refinementList?.status, category: $event } } @@ -200,6 +203,36 @@ > + + + +
diff --git a/src/app/pages/market/pages/collections/collections.page.ts b/src/app/pages/market/pages/collections/collections.page.ts index cd60213..e5ee947 100644 --- a/src/app/pages/market/pages/collections/collections.page.ts +++ b/src/app/pages/market/pages/collections/collections.page.ts @@ -34,6 +34,7 @@ export class CollectionsPage implements OnInit { spaceFilterOpen = true; categoryFilterOpen = false; priceFilterOpen = false; + statusFilterOpen = false; constructor( public filter: FilterService, diff --git a/src/app/pages/nft/pages/nft/nft.page.html b/src/app/pages/nft/pages/nft/nft.page.html index 2c6a472..aeed9e9 100644 --- a/src/app/pages/nft/pages/nft/nft.page.html +++ b/src/app/pages/nft/pages/nft/nft.page.html @@ -248,27 +248,44 @@

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

> -
- + + - + + + + + +
diff --git a/src/app/pages/nft/pages/nft/nft.page.ts b/src/app/pages/nft/pages/nft/nft.page.ts index 78a2e4d..ae1d3e5 100644 --- a/src/app/pages/nft/pages/nft/nft.page.ts +++ b/src/app/pages/nft/pages/nft/nft.page.ts @@ -47,6 +47,7 @@ import { NzNotificationService } from 'ng-zorro-antd/notification'; import { BehaviorSubject, Subscription, combineLatest, interval, map, skip, take } from 'rxjs'; import { filter } from 'rxjs/operators'; import { DataService } from '../../services/data.service'; +import { CartService } from '@components/cart/services/cart.service'; export enum ListingType { CURRENT_BIDS = 0, @@ -83,6 +84,7 @@ export class NFTPage implements OnInit, OnDestroy { preparing: $localize`Available once minted...`, view: $localize`View`, }; + public currentNft: Nft | null | undefined = null; private subscriptions$: Subscription[] = []; private nftSubscriptions$: Subscription[] = []; private collectionSubscriptions$: Subscription[] = []; @@ -109,11 +111,16 @@ export class NFTPage implements OnInit, OnDestroy { private themeService: ThemeService, private seo: SeoService, private notification: NotificationService, + private cartService: CartService, ) { // none } public ngOnInit(): void { + this.data.nft$.subscribe(nft => { + //console.log('[OnInit] Current NFT:', nft); + this.currentNft = nft; + }); this.deviceService.viewWithSearch$.next(false); this.route.params?.pipe(untilDestroyed(this)).subscribe((obj) => { const id: string | undefined = obj?.[ROUTER_UTILS.config.nft.nft.replace(':', '')]; @@ -523,6 +530,23 @@ export class NFTPage implements OnInit, OnDestroy { } } + public addToCart(nft: Nft): void { + if (nft && this.data.collection$) { + //console.log('[addToCart-this.data.collection$.value', this.data.collection$.value) + //console.log('[addToCart-this.data.nft$.value', this.data.nft$.value) + this.data.collection$.pipe(take(1)).subscribe(collection => { + if (collection) { + this.cartService.addToCart({ nft, collection, quantity: 1 }); + //console.log('Added to cart:', nft, collection); + } else { + //console.error('Collection is undefined or null'); + } + }); + } else { + //console.error('NFT is undefined or null'); + } + } + public generatedNft(nft?: Nft | null): boolean { if (!nft) { return false; diff --git a/src/app/pages/nft/services/helper.service.ts b/src/app/pages/nft/services/helper.service.ts index ae5f98a..83553bf 100644 --- a/src/app/pages/nft/services/helper.service.ts +++ b/src/app/pages/nft/services/helper.service.ts @@ -124,16 +124,22 @@ export class HelperService { } public getDate(date: any): any { - if (typeof date === 'object' && date?.toDate) { - const d = date.toDate(); - if (!(d instanceof Date) || isNaN(d.valueOf())) { - return undefined; + //console.log(`[getDate] Original input:`, date); + if (typeof date === 'object') { + if (date?.toDate) { + + const dateFromObject = date.toDate(); + //console.log(`[getDate] Object with toDate method detected, toDate result:`, dateFromObject); + return dateFromObject; + } else if (date?.seconds) { + + const dateFromSeconds = new Date(date.seconds * 1000); // Convert to milliseconds + //console.log(`[getDate] Object with seconds property detected, converted to Date:`, dateFromSeconds); + return dateFromSeconds; } - - return d; - } else { - return date || undefined; } + //console.log(`[getDate] Returning undefined, input could not be parsed as a date.`); + return undefined; } public getCountdownTitle(nft?: Nft | null): string { @@ -168,15 +174,23 @@ export class HelperService { } public isAvailableForSale(nft?: Nft | null, col?: Collection | null): boolean { + //console.log("[NFThelper-isAvailableForSale] function called"); if (!col || !nft?.availableFrom || col?.status === CollectionStatus.MINTING) { + //console.log("[NFT helper.service.ts] isAvailableForSale function returning false. nft name: " + nft?.name + ", col name: " + col?.name) return false; } - return ( + + const isAvail = ( col.approved === true && !!this.getDate(nft.availableFrom) && dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's') ); + //console.log("[NFT helper.service.ts] isAvailableForSale function returning " + isAvail + ". nft name: " + nft?.name + ", col name: " + col?.name + ". nft.availableFrom: " + nft.availableFrom.seconds); + //console.log("col.approved: " + (col.approved === true)); + //console.log("!!this.getDate(nft.availableFrom): " + !!this.getDate(nft.availableFrom) + ", this.getDate(nft.availableFrom): " + this.getDate(nft.availableFrom)); + //console.log("dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's')" + dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's')); + return isAvail; } public canBeSetForSale(nft?: Nft | null): boolean { diff --git a/src/theme/02-components/modal.less b/src/theme/02-components/modal.less index 582a03b..f20460e 100644 --- a/src/theme/02-components/modal.less +++ b/src/theme/02-components/modal.less @@ -2,6 +2,10 @@ @apply bg-backgrounds-tertiary dark:bg-backgrounds-tertiary-dark text-foregrounds-primary dark:text-foregrounds-primary-dark; } +.ant-modal-header { + @apply bg-backgrounds-tertiary dark:bg-backgrounds-tertiary-dark text-foregrounds-primary dark:text-foregrounds-primary-dark; +} + .wen-transaction-select-container { @apply flex flex-col justify-between h-full mt-10 lg:items-center lg:justify-start; } @@ -29,3 +33,8 @@ .wen-modal-tabs { @apply absolute bottom-0 w-full top-56 lg:static; } + +.wen-modal-header { + @apply flex justify-between items-center; + // Additional styling if needed +} From 2a7ac4e13b03021a80251b82863dc3903d846f6c Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Mon, 22 Jan 2024 18:12:47 -0500 Subject: [PATCH 02/24] Finished minimum viable functionality for cart checkout review, network selection and completion for bulk buys. --- .vscode/launch.json | 18 - src/app/@api/order.api.ts | 4 + .../@shell/ui/header/header.component.html | 4 +- src/app/@shell/ui/header/header.component.ts | 48 +- src/app/@shell/ui/header/header.module.ts | 2 +- src/app/components/cart/cart.module.ts | 29 +- .../cart-modal/cart-modal.component.html | 25 +- .../cart-modal/cart-modal.component.ts | 39 +- .../checkout/checkout-overlay.component.html | 251 +++++++++- .../checkout/checkout-overlay.component.ts | 458 +++++++++++++++++- .../components/cart/services/cart.service.ts | 39 ++ .../components/nft-card/nft-card.component.ts | 2 +- .../pages/collection/nfts/nfts.page.html | 3 +- .../pages/collection/nfts/nfts.page.ts | 36 +- src/app/pages/nft/pages/nft/nft.page.ts | 2 +- 15 files changed, 858 insertions(+), 102 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 78837d7..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "NEW Launch npm start", - "runtimeExecutable": "npm", - "runtimeArgs": [ - "start" - ], - "skipFiles": [ - "/**" - ], - "console": "integratedTerminal" - } - ] -} diff --git a/src/app/@api/order.api.ts b/src/app/@api/order.api.ts index 4ddae44..414048a 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,9 @@ 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 f84fa47..6799c89 100644 --- a/src/app/@shell/ui/header/header.component.html +++ b/src/app/@shell/ui/header/header.component.html @@ -275,7 +275,9 @@ - + + + = new BehaviorSubject([]); @@ -91,6 +98,8 @@ export class HeaderComponent implements OnInit, OnDestroy { public cartItemCount = 0; private cartItemsSubscription!: Subscription; + @Output() openCartModal = new EventEmitter(); + constructor( public auth: AuthService, public deviceService: DeviceService, @@ -105,7 +114,8 @@ export class HeaderComponent implements OnInit, OnDestroy { private cd: ChangeDetectorRef, private nzNotification: NzNotificationService, private checkoutService: CheckoutService, - private cartService: CartService, + public cartService: CartService, + private modalService: NzModalService, ) {} public ngOnInit(): void { @@ -234,6 +244,14 @@ export class HeaderComponent implements OnInit, OnDestroy { public async onOpenCheckout(): Promise { const t = this.transaction$.getValue(); + console.log('[header-onOpenCheckout] transaction: ', t) + console.log('[header-onOpenCheckout] t?.payload.type: ', t?.payload.type) + if (t?.payload.type == TransactionPayloadType.NFT_PURCHASE_BULK) { + console.log('[header-onOpenCheckout] !t?.payload.type && t?.payload.type == TransactionPayloadType.NFT_PURCHASE_BULK equals true and isCartCheckoutOpen set to true') + this.openCartModal.emit(); + this.openCheckoutOverlay(); + } + if (!t?.payload.nft || !t.payload.collection) { return; } @@ -257,6 +275,24 @@ export class HeaderComponent implements OnInit, OnDestroy { } } + private openCheckoutOverlay(): void { + const cartItems = this.cartService.getCartItems().getValue(); + + this.modalService.create({ + nzTitle: 'Checkout', + nzContent: CheckoutOverlayComponent, + nzComponentParams: { items: cartItems }, + nzFooter: null, + nzWidth: '80%', + }); + } + + public handleOpenCartModal(): void { + this.openCartModal.emit(); + } + + + public get filesizes(): typeof FILE_SIZES { return FILE_SIZES; } @@ -278,6 +314,10 @@ export class HeaderComponent implements OnInit, OnDestroy { this.isCheckoutOpen = false; } + public closeCartCheckout() { + this.isCartCheckoutOpen = false; + } + public goToMyProfile(): void { if (this.member$.value?.uid) { this.router.navigate([ @@ -393,6 +433,12 @@ export class HeaderComponent implements OnInit, OnDestroy { this.cartService.showCart(); } + public handleCartCheckout(): void { + this.isCartCheckoutOpen = true; + this.cd.markForCheck(); + } + + public ngOnDestroy(): void { this.cancelAccessSubscriptions(); this.subscriptionNotification$?.unsubscribe(); diff --git a/src/app/@shell/ui/header/header.module.ts b/src/app/@shell/ui/header/header.module.ts index 3b1c6f2..0d261e9 100644 --- a/src/app/@shell/ui/header/header.module.ts +++ b/src/app/@shell/ui/header/header.module.ts @@ -51,7 +51,7 @@ import { CartModule } from '@components/cart/cart.module'; NftCheckoutModule, MobileMenuModule, MobileHeaderModule, - CartModule + CartModule, ], exports: [HeaderComponent], }) diff --git a/src/app/components/cart/cart.module.ts b/src/app/components/cart/cart.module.ts index b4be732..d1cb922 100644 --- a/src/app/components/cart/cart.module.ts +++ b/src/app/components/cart/cart.module.ts @@ -13,6 +13,18 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; import { NzFormModule } from 'ng-zorro-antd/form'; import { NzInputModule } from 'ng-zorro-antd/input'; +import { NzDividerModule } from 'ng-zorro-antd/divider'; +import { NzTableModule } from 'ng-zorro-antd/table'; +import { NzTagModule } from 'ng-zorro-antd/tag'; +import { NzEmptyModule } from 'ng-zorro-antd/empty'; +import { TermsAndConditionsModule } from '@components/terms-and-conditions/terms-and-conditions.module'; +import { TimeModule } from '@core/pipes/time/time.module'; +import { CountdownTimeModule } from '@core/pipes/countdown-time/countdown-time.module'; +import { NzAlertModule } from 'ng-zorro-antd/alert'; +import { NetworkModule } from '@components/network/network.module'; +import { RouterModule } from '@angular/router'; +import { WalletDeeplinkModule } from '@components/wallet-deeplink/wallet-deeplink.module'; +import { NzRadioModule } from 'ng-zorro-antd/radio'; @NgModule({ declarations: [ @@ -33,7 +45,22 @@ import { NzInputModule } from 'ng-zorro-antd/input'; ReactiveFormsModule, NzFormModule, NzInputModule, + NzDividerModule, + NzTableModule, + NzTagModule, + NzEmptyModule, + TermsAndConditionsModule, + TimeModule, + CountdownTimeModule, + NzAlertModule, + NetworkModule, + RouterModule, + WalletDeeplinkModule, + NzRadioModule, + ], + exports: [ + CartModalComponent, + CheckoutOverlayComponent, ], - exports: [CartModalComponent], }) export class CartModule {} diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.html b/src/app/components/cart/components/cart-modal/cart-modal.component.html index acbddd6..83207b6 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.html +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.html @@ -50,19 +50,6 @@
Qty Added / Available
- - -
@@ -84,15 +71,6 @@ -
@@ -111,7 +89,6 @@ -
Remove
-
diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.ts b/src/app/components/cart/components/cart-modal/cart-modal.component.ts index adcc387..f16f231 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.ts +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.ts @@ -4,7 +4,9 @@ import { OnInit, OnDestroy, ChangeDetectorRef, - ChangeDetectionStrategy + ChangeDetectionStrategy, + EventEmitter, + Output, } from '@angular/core'; import { Nft, @@ -14,12 +16,12 @@ import { import { Subscription, forkJoin, map, take, catchError, of } from 'rxjs'; import { CartService, CartItem } from './../../services/cart.service'; import { AuthService } from '@components/auth/services/auth.service'; -//import { ActivatedRoute, Router } from '@angular/router'; import { ROUTER_UTILS } from '@core/utils/router.utils'; import { NzModalService } from 'ng-zorro-antd/modal'; import { CheckoutOverlayComponent } from '../checkout/checkout-overlay.component'; import { NftApi } from '@api/nft.api'; -//import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { NzNotificationService } from 'ng-zorro-antd/notification'; + @Component({ selector: 'app-cart-modal', @@ -33,6 +35,9 @@ export class CartModalComponent implements OnInit, OnDestroy { public nftPath: string = ROUTER_UTILS.config.nft.root; public cartItemsQuantities: number[] = []; cartItemPrices: { [key: string]: { originalPrice: number, discountedPrice: number } } = {}; + isCartCheckoutOpen = false; + + @Output() onCartCheckout = new EventEmitter(); constructor( public cartService: CartService, @@ -40,6 +45,7 @@ export class CartModalComponent implements OnInit, OnDestroy { public auth: AuthService, private modalService: NzModalService, private nftApi: NftApi, + private notification: NzNotificationService, ) {} ngOnInit() { @@ -96,7 +102,6 @@ export class CartModalComponent implements OnInit, OnDestroy { this.cartService.updateCartItems(freshCartItems); - // Perform existing refresh operations this.cartItemsStatus = freshCartItems.map(item => this.cartItemStatus(item)); this.cartItemsQuantities = freshCartItems.map(item => this.cartItemSaleAvailableQty(item)); freshCartItems.forEach(item => { @@ -110,9 +115,8 @@ export class CartModalComponent implements OnInit, OnDestroy { this.cd.markForCheck(); }, error => { - console.error('Error while refreshing cart items:', error); - // Handle the error appropriately - // Maybe show a user-friendly message or perform some recovery logic + console.error('Error while refreshing cart items: ', error); + this.notification.error($localize`Error while refreshing cart items: ` + error, ''); } ); } @@ -159,7 +163,7 @@ export class CartModalComponent implements OnInit, OnDestroy { return finalPrice; } - private calcPrice(item: CartItem, discount: number): number { + public calcPrice(item: CartItem, discount: number): number { const itemPrice = item.nft?.availablePrice || item.nft?.price || 0; return this.calc(itemPrice, discount); // assuming calc method applies the discount } @@ -182,15 +186,15 @@ export class CartModalComponent implements OnInit, OnDestroy { return availQty; } - handleClose(): void { + public handleClose(): void { this.cartService.hideCart(); } + /* public handleCheckout(): void { const cartItems = this.cartService.getCartItems().getValue(); //console.log('Proceeding to checkout with items:', cartItems); - // Open the checkout overlay here this.modalService.create({ nzTitle: 'Checkout', nzContent: CheckoutOverlayComponent, @@ -198,6 +202,21 @@ export class CartModalComponent implements OnInit, OnDestroy { items: cartItems }, nzFooter: null, + nzWidth: '80%' + }); + } + */ + + public handleCartCheckout(): void { + const cartItems = this.cartService.getCartItems().getValue(); + + this.modalService.create({ + nzTitle: 'Checkout', + nzContent: CheckoutOverlayComponent, + nzComponentParams: { items: cartItems }, + nzFooter: null, + nzWidth: '80%', + nzOnOk: () => this.isCartCheckoutOpen = false // Optionally handle the modal close event }); } diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.html b/src/app/components/cart/components/checkout/checkout-overlay.component.html index 7abd0d0..e6282b2 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.html +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.html @@ -1,7 +1,246 @@ - -
- -
+ +
+ Notice {{ unavailableItemCount }} items were not included in the checkout due to not being available for sale. +
+ +
+

Network/Token: {{ group.tokenSymbol }}

+ + + + + NFT Name + Collection Name + Quantity Added + Price ({{ group.tokenSymbol }}) + Line Item Total + + + + + + {{ item.nft.name }} + {{ item.collection.name }} + {{ item.quantity }} + + {{ (item.salePrice | formatToken:(item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network):true:true) | async }} + + + {{ ((item.quantity * item.salePrice) | formatToken:(item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network):true:true) | async }} + + + + + Total + {{ group.totalQuantity }} + + + + {{ (group.totalPrice / 1000000) }} {{ group.tokenSymbol }} + + + + + + +
- - +
+ + + + + + + + + + + + + + + +
+
+ + + + + + +
+ + + + + +
+ Your NFTs will be locked for purchase for {{ lockTime }} + minutes. +
+
+
+ + +
+
+ +
+ {{ + expiryTicker$ | async | countdownTime + }} + + remaining to make the transfer. + Expired +
+
+ + + + + + + + +
+
+ + +
+
+
Transaction history
+ +
+
+
{{ t.date | Time }}
+ {{ t.label }} +
{{ t.label }}
+
+
+
+ + +
+
+ + +
+
+
Transaction history
+ +
+
+
{{ t.date | Time }}
+ {{ t.label }} +
{{ t.label }}
+
+
+
+ +
+
+ +
+
Transaction complete. Congratulations.
+
+ +
+ + + + +
+
+
diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.ts b/src/app/components/cart/components/checkout/checkout-overlay.component.ts index cf06037..7744df9 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.ts +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.ts @@ -1,14 +1,462 @@ -import { Component, Input } from '@angular/core'; -import { CartItem } from './../../services/cart.service'; +import { Component, Input, OnInit, ChangeDetectorRef, ChangeDetectionStrategy, Output, EventEmitter, } from '@angular/core'; +import { CartItem, CartService } from './../../services/cart.service'; +import { + CollectionType, + Nft, + Timestamp, + Transaction, + TransactionType, + TRANSACTION_AUTO_EXPIRY_MS, + NftPurchaseRequest, + Network, +} from '@build-5/interfaces'; +import { AuthService } from '@components/auth/services/auth.service'; +import { NotificationService } from '@core/services/notification'; +import { OrderApi } from '@api/order.api'; +import { NftApi } from '@api/nft.api'; +//import { FileApi } from '@api/file.api'; +import { BehaviorSubject, firstValueFrom, interval, Subscription, take } from 'rxjs'; +import { TransactionService } from '@core/services/transaction'; +import { getItem, removeItem, setItem, StorageItem } from '@core/utils'; +import dayjs from 'dayjs'; +import { HelperService } from '@pages/nft/services/helper.service'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { ROUTER_UTILS } from '@core/utils/router.utils'; +import { Router } from '@angular/router'; +import { NzNotificationService } from 'ng-zorro-antd/notification'; +export enum StepType { + CONFIRM = 'Confirm', + TRANSACTION = 'Transaction', + WAIT = 'Wait', + COMPLETE = 'Complete', +} + +interface GroupedCartItem { + tokenSymbol: string; + items: CartItem[]; + totalQuantity: number; + totalPrice: number; +} + +interface HistoryItem { + uniqueId: string; + date: dayjs.Dayjs | Timestamp | null; + label: string; + transaction?: Transaction; + link?: string; +} + +@UntilDestroy() @Component({ selector: 'app-checkout-overlay', templateUrl: './checkout-overlay.component.html', styleUrls: ['./checkout-overlay.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CheckoutOverlayComponent { +export class CheckoutOverlayComponent implements OnInit { + @Input() currentStep = StepType.CONFIRM; @Input() items: CartItem[] = []; - // Implement your payment form logic here + @Input() set isOpen(value: boolean) { + this._isOpen = value; + //this.checkoutService.modalOpen$.next(value); + } + @Output() wenOnClose = new EventEmitter(); + groupedCartItems: GroupedCartItem[] = []; + unavailableItemCount = 0; + cartItemPrices: { [key: string]: { originalPrice: number, discountedPrice: number } } = {}; + public agreeTermsConditions = false; + public transaction$: BehaviorSubject = new BehaviorSubject(undefined); + public history: HistoryItem[] = []; + public expiryTicker$: BehaviorSubject = new BehaviorSubject(null); + public invalidPayment = false; + public receivedTransactions = false; + public targetAddress?: string; + public targetAmount?: number; + public purchasedNfts?: Nft[] | null; + private _isOpen = false; + public stepType = StepType; + public selectedNetwork: string | null = null; + public mintingDataNetwork: Network | undefined; + + private transSubscription?: Subscription; + public nftPath = ROUTER_UTILS.config.nft.root; + + constructor( + private cartService: CartService, + private auth: AuthService, + private notification: NotificationService, + private orderApi: OrderApi, + public transactionService: TransactionService, + public helper: HelperService, + private cd: ChangeDetectorRef, + private nftApi: NftApi, + //private fileApi: FileApi, + private router: Router, + private nzNotification: NzNotificationService, + ) {} + + ngOnInit() { + console.log('checkout-overlay ngOnInit called, running groupItems code.'); + this.groupItems(); + this.receivedTransactions = false; + const listeningToTransaction: string[] = []; + this.transaction$.pipe(untilDestroyed(this)).subscribe((val) => { + console.log('transaction val: ', val); + if (val && val.type === TransactionType.ORDER) { + this.targetAddress = val.payload.targetAddress; + this.targetAmount = val.payload.amount; + const expiresOn: dayjs.Dayjs = dayjs(val.payload.expiresOn!.toDate()); + if (expiresOn.isBefore(dayjs()) || val.payload?.void || val.payload?.reconciled) { + // It's expired. + removeItem(StorageItem.CheckoutTransaction); + } + if (val.linkedTransactions && val.linkedTransactions?.length > 0) { + this.currentStep = StepType.WAIT; + // Listen to other transactions. + for (const tranId of val.linkedTransactions) { + if (listeningToTransaction.indexOf(tranId) > -1) { + continue; + } + + listeningToTransaction.push(tranId); + this.orderApi + .listen(tranId) + .pipe(untilDestroyed(this)) + .subscribe(this.transaction$); + } + } else if (!val.linkedTransactions || val.linkedTransactions.length === 0) { + this.currentStep = StepType.TRANSACTION; + } + + this.expiryTicker$.next(expiresOn); + } + + if (val && val.type === TransactionType.PAYMENT && val.payload.reconciled === true) { + this.pushToHistory( + val, + val.uid + '_payment_received', + val.createdOn, + $localize`Payment received.`, + (val).payload?.chainReference, + ); + } + + if ( + val && + val.type === TransactionType.PAYMENT && + val.payload.reconciled === true && + (val).payload.invalidPayment === false + ) { + // Let's add delay to achive nice effect. + setTimeout(() => { + this.pushToHistory( + val, + val.uid + '_confirming_trans', + dayjs(), + $localize`Confirming transaction.`, + ); + }, 1000); + + setTimeout(() => { + this.pushToHistory( + val, + val.uid + '_confirmed_trans', + dayjs(), + $localize`Transaction confirmed.`, + ); + this.receivedTransactions = true; + this.currentStep = StepType.COMPLETE; + + const purchasedItemIds = this.getPurchasedItemIds(val); + this.cartService.removeItemsFromCart(purchasedItemIds); + + + + this.cd.markForCheck(); + }, 2000); + + // Load purchased NFTs. + if (val.payload.nftOrders && val.payload.nftOrders.length > 0) { + this.purchasedNfts = this.purchasedNfts || []; + val.payload.nftOrders.forEach(nftOrder => { + firstValueFrom(this.nftApi.listen(nftOrder.nft)).then((obj) => { + if (obj !== null && obj !== undefined) { + const purchasedNft = obj as Nft; // Type assertion + + this.purchasedNfts = [...(this.purchasedNfts || []), purchasedNft]; + this.cd.markForCheck(); + } + }); + }); + } + + } + + if ( + val && + val.type === TransactionType.CREDIT && + val.payload.reconciled === true && + val.ignoreWallet === false && + !val.payload?.walletReference?.chainReference + ) { + this.pushToHistory( + val, + val.uid + '_false', + val.createdOn, + $localize`Invalid amount received. Refunding transaction...`, + ); + } + + const markInvalid = () => { + setTimeout(() => { + this.currentStep = StepType.TRANSACTION; + this.invalidPayment = true; + this.cd.markForCheck(); + }, 2000); + }; + + if ( + val && + val.type === TransactionType.CREDIT && + val.payload.reconciled === true && + val.ignoreWallet === true && + !val.payload?.walletReference?.chainReference + ) { + this.pushToHistory( + val, + val.uid + '_false', + val.createdOn, + $localize`Invalid transaction.You must gift storage deposit.`, + ); + markInvalid(); + } + + if ( + val && + val.type === TransactionType.CREDIT && + val.payload.reconciled === true && + val.payload?.walletReference?.chainReference + ) { + this.pushToHistory( + val, + val.uid + '_true', + dayjs(), + $localize`Invalid payment refunded.`, + val.payload?.walletReference?.chainReference, + ); + + // Let's go back to wait. With slight delay so they can see this. + markInvalid(); + } + + this.cd.markForCheck(); + }); + + if (getItem(StorageItem.CheckoutTransaction)) { + this.transSubscription = this.orderApi + .listen(getItem(StorageItem.CheckoutTransaction)) + .subscribe(this.transaction$); + } + + // Run ticker. + const int: Subscription = interval(1000) + .pipe(untilDestroyed(this)) + .subscribe(() => { + this.expiryTicker$.next(this.expiryTicker$.value); + + // If it's in the past. + if (this.expiryTicker$.value) { + const expiresOn: dayjs.Dayjs = dayjs(this.expiryTicker$.value).add( + TRANSACTION_AUTO_EXPIRY_MS, + 'ms', + ); + if (expiresOn.isBefore(dayjs())) { + this.expiryTicker$.next(null); + removeItem(StorageItem.CheckoutTransaction); + int.unsubscribe(); + this.reset(); + } + } + }); + } + + groupItems() { + console.log('groupItems function called.') + const groups: { [tokenSymbol: string]: GroupedCartItem } = {}; + this.items.forEach(item => { + const tokenSymbol = (item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network) || 'Unknown'; + const discount = this.discount(item); + const originalPrice = this.calcPrice(item, 1); + const discountedPrice = this.calcPrice(item, discount); + const price = (this.discount(item) < 1) ? discountedPrice : originalPrice; + item.salePrice = price; + + if (this.cartService.isCartItemAvailableForSale(item)) { + if (!groups[tokenSymbol]) { + groups[tokenSymbol] = { tokenSymbol, items: [], totalQuantity: 0, totalPrice: 0 }; + } + groups[tokenSymbol].items.push(item); + groups[tokenSymbol].totalQuantity += item.quantity; + groups[tokenSymbol].totalPrice += item.quantity * item.salePrice; + } else { + this.unavailableItemCount++; + } + + console.log('Cart item loop finished, group: ', groups[tokenSymbol]) + }); + + this.groupedCartItems = Object.values(groups); + } + + private calcPrice(item: CartItem, discount: number): number { + return this.cartService.calcPrice(item, discount); + } + + private discount(item: CartItem): number { + return this.cartService.discount(item.collection, item.nft); + } + + public isCartItemAvailableForSale(item: CartItem): any { + return this.cartService.isCartItemAvailableForSale(item); + } + + public reset(): void { + this.receivedTransactions = false; + this.isOpen = false; + this.currentStep = StepType.CONFIRM; + this.purchasedNfts = undefined; + this.cd.markForCheck(); + } + + public close(): void { + this.reset(); + this.wenOnClose.next(); + } + + public getRecords(): Nft[] | null | undefined { + return this.purchasedNfts || null; + } + + private getPurchasedItemIds(transaction: Transaction): string[] { + return transaction.payload.nftOrders?.map(item => item.nft) || []; + } + + public pushToHistory( + transaction: Transaction, + uniqueId: string, + date?: dayjs.Dayjs | Timestamp | null, + text?: string, + link?: string, + ): void { + if ( + this.history.find((s) => { + return s.uniqueId === uniqueId; + }) + ) { + return; + } + + if (date && text) { + this.history.unshift({ + transaction, + uniqueId: uniqueId, + date: date, + label: text, + link: link, + }); + } + } + + public get lockTime(): number { + return TRANSACTION_AUTO_EXPIRY_MS / 1000 / 60; + } + + public async initiateBulkOrder(): Promise { + const selectedGroup = this.groupedCartItems.find(group => group.tokenSymbol === this.selectedNetwork); + + if (selectedGroup && selectedGroup.items.length > 0) { + const firstItem = selectedGroup.items[0]; + this.mintingDataNetwork = firstItem.nft?.placeholderNft ? firstItem.collection?.mintingData?.network : firstItem.nft?.mintingData?.network; + } + + if (!selectedGroup || selectedGroup.items.length === 0) { + console.warn('No network selected or no items in the selected network.'); + this.nzNotification.error($localize`No network selected or no items in the selected network.`, ''); + return; + } + + // Convert only the NFTs from the selected group + const nfts = this.convertGroupedCartItemsToNfts(selectedGroup); + + if (nfts.length === 0) { + console.warn('No NFTs to purchase.'); + this.nzNotification.error($localize`No NFTs to purchase.`, ''); + return; + } + + await this.proceedWithBulkOrder(nfts); + } + + + convertGroupedCartItemsToNfts(selectedGroup: GroupedCartItem): NftPurchaseRequest[] { + const nfts: NftPurchaseRequest[] = []; + + selectedGroup.items.forEach(item => { + if (item.nft && item.collection) { + const nftData: NftPurchaseRequest = { + collection: item.collection.uid, + }; + + if (item.collection.type === CollectionType.CLASSIC) { + nftData.nft = item.nft.uid; + } + + // If owner is set, CollectionType is not relevant. + if (item.nft.owner) { + nftData.nft = item.nft.uid; + } + + nfts.push(nftData); + } + }); + + return nfts; + } + + public async proceedWithBulkOrder(nfts: NftPurchaseRequest[]): Promise { + const selectedGroup = this.groupedCartItems.find(group => group.tokenSymbol === this.selectedNetwork); + if (!selectedGroup) { + console.warn('No network selected or no items in the selected network.'); + this.nzNotification.error($localize`No network selected or no items in the selected network.`, ''); + return; + } + + if (nfts.length === 0 || !this.agreeTermsConditions) { + console.warn('No NFTs to purchase or terms and conditions are not agreed.'); + this.nzNotification.error($localize`No NFTs to purchase or terms and conditions are not agreed.`, ''); + return; + } + + const bulkPurchaseRequest = { + orders: nfts + }; + + await this.auth.sign(bulkPurchaseRequest, async (signedRequest, finish) => { + this.notification + .processRequest(this.orderApi.orderNfts(signedRequest), $localize`Bulk order created.`, finish) + .subscribe((transaction: Transaction | undefined) => { + if (transaction) { + this.transSubscription?.unsubscribe(); + setItem(StorageItem.CheckoutTransaction, transaction.uid); + this.transSubscription = this.orderApi.listen(transaction.uid).subscribe(this.transaction$); + this.pushToHistory(transaction, transaction.uid, dayjs(), $localize`Waiting for transaction...`); + } else { + console.error('Transaction failed or did not return a valid transaction.'); + this.nzNotification.error($localize`Transaction failed or did not return a valid transaction.`, ''); + } + }); + }); + } - // ... other methods } diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts index 009ec54..00db18f 100644 --- a/src/app/components/cart/services/cart.service.ts +++ b/src/app/components/cart/services/cart.service.ts @@ -3,6 +3,7 @@ import { BehaviorSubject } from 'rxjs'; import { Nft, Collection, + MIN_AMOUNT_TO_TRANSFER, } from '@build-5/interfaces'; import { getItem, setItem, StorageItem } from './../../../@core/utils/local-storage.utils'; import { NzNotificationService } from 'ng-zorro-antd/notification'; @@ -13,6 +14,7 @@ export interface CartItem { nft: Nft; collection: Collection; quantity: number; + salePrice: number; } @Injectable({ @@ -70,6 +72,12 @@ export class CartService { //console.log('[CartService-removeFromCart] Cart updated:', updatedCartItems); } + public removeItemsFromCart(itemIds: string[]): void { + const updatedCartItems = this.cartItemsSubject.value.filter(item => !itemIds.includes(item.nft.uid)); + this.cartItemsSubject.next(updatedCartItems); + this.saveCartItems(); + } + public getCartItems(): BehaviorSubject { //console.log('[CartService] getCartItems function called.'); return this.cartItemsSubject; @@ -120,4 +128,35 @@ export class CartService { //console.log("[service-getAvailableNftQuantity] returning 0, isAvailableForSale: " + isAvailableForSale + ". placeholder: " + cartItem.nft.placeholderNft); return 0; } + + public discount(collection?: Collection | null, nft?: Nft | null): number { + if (!collection?.space || !this.auth.member$.value || nft?.owner) { + return 1; + } + + const spaceRewards = (this.auth.member$.value.spaces || {})[collection.space]; + const descDiscounts = [...(collection.discounts || [])].sort((a, b) => b.amount - a.amount); + for (const discount of descDiscounts) { + const awardStat = (spaceRewards?.awardStat || {})[discount.tokenUid!]; + const memberTotalReward = awardStat?.totalReward || 0; + if (memberTotalReward >= discount.tokenReward) { + return 1 - discount.amount; + } + } + return 1; + } + + public calc(amount: number | null | undefined, discount: number): number { + let finalPrice = Math.ceil((amount || 0) * discount); + if (finalPrice < MIN_AMOUNT_TO_TRANSFER) { + finalPrice = MIN_AMOUNT_TO_TRANSFER; + } + + return finalPrice; + } + + public calcPrice(item: CartItem, discount: number): number { + const itemPrice = item.nft?.availablePrice || item.nft?.price || 0; + return this.calc(itemPrice, discount); // assuming calc method applies the discount + } } diff --git a/src/app/components/nft/components/nft-card/nft-card.component.ts b/src/app/components/nft/components/nft-card/nft-card.component.ts index f755463..dd804a1 100644 --- a/src/app/components/nft/components/nft-card/nft-card.component.ts +++ b/src/app/components/nft/components/nft-card/nft-card.component.ts @@ -191,7 +191,7 @@ export class NftCardComponent { if (nft && collection) { console.log('[NftCardComponent] Adding item to cart:', nft, collection); - this.cartService.addToCart({ nft, collection, quantity: 1 }); + this.cartService.addToCart({ nft, collection, quantity: 1, salePrice: 0 }); // Optionally, provide feedback to the user } else { // Handle the case when nft or collection is null or undefined diff --git a/src/app/pages/collection/pages/collection/nfts/nfts.page.html b/src/app/pages/collection/pages/collection/nfts/nfts.page.html index 4a173b8..407b310 100644 --- a/src/app/pages/collection/pages/collection/nfts/nfts.page.html +++ b/src/app/pages/collection/pages/collection/nfts/nfts.page.html @@ -14,7 +14,8 @@ {{ state.nbHits | number }} records
- + +
{{ sweepCount }} diff --git a/src/app/pages/collection/pages/collection/nfts/nfts.page.ts b/src/app/pages/collection/pages/collection/nfts/nfts.page.ts index 771dd79..c29badf 100644 --- a/src/app/pages/collection/pages/collection/nfts/nfts.page.ts +++ b/src/app/pages/collection/pages/collection/nfts/nfts.page.ts @@ -5,7 +5,6 @@ import { Input, OnChanges, OnInit, - AfterViewInit, } from '@angular/core'; import { NftApi } from '@api/nft.api'; import { CollectionApi } from '@api/collection.api'; @@ -49,7 +48,7 @@ export enum HOT_TAGS { // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection changeDetection: ChangeDetectionStrategy.Default, }) -export class CollectionNFTsPage implements OnInit, OnChanges, AfterViewInit { +export class CollectionNFTsPage implements OnInit, OnChanges { @Input() public collectionId?: string | null; config?: InstantSearchConfig; sections = marketSections; @@ -66,7 +65,6 @@ export class CollectionNFTsPage implements OnInit, OnChanges, AfterViewInit { availableNftsCount = 0; collection: Collection | null = null; - constructor( public filter: FilterService, public deviceService: DeviceService, @@ -93,7 +91,7 @@ export class CollectionNFTsPage implements OnInit, OnChanges, AfterViewInit { } }, error: err => { - console.error('Error fetching collection:', err); + //console.error('Error fetching collection:', err); this.notification.error($localize`Error occurred while fetching collection.`, ''); } }); @@ -105,23 +103,10 @@ export class CollectionNFTsPage implements OnInit, OnChanges, AfterViewInit { this.availableNftsCount = count; }); - setTimeout(() => { - this.sweepCount = 1; // or whatever the initial value should be - this.cd.markForCheck(); // trigger change detection manually - }, 0); - - console.log('Component initialized, sweepCount:', this.sweepCount); - // Algolia change detection bug fix setInterval(() => this.cd.markForCheck(), 500); } - ngAfterViewInit() { - console.log('View fully initialized, slider should now be rendered, sweepCount: ', this.sweepCount); - this.cd.detectChanges(); - this.cd.markForCheck(); - } - public ngOnChanges(): void { // TODO comeup with better process. setTimeout(() => { @@ -149,9 +134,7 @@ export class CollectionNFTsPage implements OnInit, OnChanges, AfterViewInit { if (hits && hits.length > 0 && this.collection) { this.originalNfts = hits; this.collectionNftStateService.setListedNfts(hits, this.collection); - console.log('Original hits captured:', this.originalNfts); } else { - console.log('Received empty hits array or collection is not available, ignoring to preserve existing listedNfts.'); } } @@ -160,10 +143,8 @@ export class CollectionNFTsPage implements OnInit, OnChanges, AfterViewInit { } public convertAllToSoonaverseModel = (algoliaItems: any[]) => { - // Capture the original hits this.captureOriginalHits(algoliaItems); - // Proceed with the transformation const transformedItems = algoliaItems.map((algolia) => ({ ...algolia, availableFrom: Timestamp.fromMillis(+algolia.availableFrom), @@ -191,15 +172,11 @@ export class CollectionNFTsPage implements OnInit, OnChanges, AfterViewInit { filter((collection): collection is Collection => collection !== undefined), switchMap((collection: Collection) => { const listedNfts = this.collectionNftStateService.getListedNfts(); - console.log('[sweep] Listed NFTs:', listedNfts); - // Filter NFTs based on their availability for sale const nftsForSale = listedNfts.filter(nft => this.cartService.isNftAvailableForSale(nft, collection) ); - console.log('[sweep] NFTs for sale:', nftsForSale); - // Sort the NFTs by price and take the top 'count' NFTs const nftsToAdd = nftsForSale .slice(0, Math.min(count, 20)) .sort((a, b) => { @@ -208,26 +185,21 @@ export class CollectionNFTsPage implements OnInit, OnChanges, AfterViewInit { return priceA - priceB; }); - // Add the selected NFTs to the cart nftsToAdd.forEach(nft => { - const cartItem = { nft, collection, quantity: 1 }; + const cartItem = { nft, collection, quantity: 1, salePrice: 0 }; this.cartService.addToCart(cartItem); }); - console.log('[sweep] NFTs added to cart:', nftsToAdd); this.notification.success($localize`NFTs swept into your cart, open cart to review added items.`, ''); - return nftsToAdd; // You can return something relevant here if needed + return nftsToAdd; }), takeUntil(this.destroy$) ) .subscribe({ next: nftsToAdd => { - // Handle successful addition - console.log('[sweep] NFTs successfully added to cart:', nftsToAdd); }, error: err => { - console.error('[sweep] Error fetching collection:', err); this.notification.error($localize`Error occurred while fetching collection.`, ''); } }); diff --git a/src/app/pages/nft/pages/nft/nft.page.ts b/src/app/pages/nft/pages/nft/nft.page.ts index ae1d3e5..90f5bdb 100644 --- a/src/app/pages/nft/pages/nft/nft.page.ts +++ b/src/app/pages/nft/pages/nft/nft.page.ts @@ -536,7 +536,7 @@ export class NFTPage implements OnInit, OnDestroy { //console.log('[addToCart-this.data.nft$.value', this.data.nft$.value) this.data.collection$.pipe(take(1)).subscribe(collection => { if (collection) { - this.cartService.addToCart({ nft, collection, quantity: 1 }); + this.cartService.addToCart({ nft, collection, quantity: 1, salePrice: 0 }); //console.log('Added to cart:', nft, collection); } else { //console.error('Collection is undefined or null'); From 3a01530aa23499ccd1b335684ffa6182b5081b92 Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Wed, 24 Jan 2024 16:22:12 -0500 Subject: [PATCH 03/24] Initial bug testing and fixing finished --- .../@core/services/router/router.service.ts | 6 +- .../@shell/ui/header/header.component.html | 11 +- src/app/@shell/ui/header/header.component.ts | 5 +- src/app/components/cart/cart.module.ts | 8 +- .../cart-modal/cart-modal.component.html | 18 +-- .../cart-modal/cart-modal.component.ts | 84 +++++++------ .../checkout/checkout-overlay.component.html | 55 +++++--- .../checkout/checkout-overlay.component.ts | 119 ++++++++++++------ .../components/cart/services/cart.service.ts | 41 +++--- .../nft-checkout/nft-checkout.component.ts | 3 + src/app/pages/award/pages/new/new.page.ts | 4 +- .../pages/collection/nfts/nfts.page.ts | 12 +- src/app/pages/nft/pages/nft/nft.page.html | 17 ++- src/app/pages/nft/pages/nft/nft.page.ts | 33 ++++- src/app/pages/nft/services/helper.service.ts | 20 +-- 15 files changed, 267 insertions(+), 169 deletions(-) diff --git a/src/app/@core/services/router/router.service.ts b/src/app/@core/services/router/router.service.ts index dfdfb6f..b4e2399 100644 --- a/src/app/@core/services/router/router.service.ts +++ b/src/app/@core/services/router/router.service.ts @@ -22,11 +22,11 @@ export class RouterService { public urlToNewToken = '/' + ROUTER_UTILS.config.token.root + '/new'; constructor(private router: Router, private deviceService: DeviceService) { - //this.router.events.pipe( + // this.router.events.pipe( // filter((event: Event): event is NavigationEnd => event instanceof NavigationEnd) - //).subscribe((event: NavigationEnd) => { + // ).subscribe((event: NavigationEnd) => { // console.log('Navigation Event:', event); - //}); + // }); this.updateVariables(); diff --git a/src/app/@shell/ui/header/header.component.html b/src/app/@shell/ui/header/header.component.html index 6799c89..736b958 100644 --- a/src/app/@shell/ui/header/header.component.html +++ b/src/app/@shell/ui/header/header.component.html @@ -47,7 +47,7 @@ nz-button nzType="default" nzShape="circle" - class="relative inline-flex items-center justify-center border-0 wen-header-button" + class="relative inline-flex items-center justify-center border-0 wen-header-button ml-0 mr-2" (click)="openShoppingCart()" > @@ -181,7 +181,7 @@ nz-button nzType="default" nzShape="circle" - class="relative inline-flex items-center justify-center border-0 wen-header-button" + class="relative inline-flex items-center justify-center border-0 wen-header-button ml-0 mr-1" (click)="openShoppingCart()" > @@ -190,9 +190,6 @@ -
 
-
 
- - diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.ts b/src/app/components/cart/components/cart-modal/cart-modal.component.ts index f16f231..c79caa0 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.ts +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.ts @@ -16,6 +16,7 @@ import { import { Subscription, forkJoin, map, take, catchError, of } from 'rxjs'; import { CartService, CartItem } from './../../services/cart.service'; import { AuthService } from '@components/auth/services/auth.service'; +import { Router } from '@angular/router'; import { ROUTER_UTILS } from '@core/utils/router.utils'; import { NzModalService } from 'ng-zorro-antd/modal'; import { CheckoutOverlayComponent } from '../checkout/checkout-overlay.component'; @@ -24,7 +25,7 @@ import { NzNotificationService } from 'ng-zorro-antd/notification'; @Component({ - selector: 'app-cart-modal', + selector: 'wen-app-cart-modal', templateUrl: './cart-modal.component.html', styleUrls: ['./cart-modal.component.less'], changeDetection: ChangeDetectionStrategy.OnPush @@ -34,11 +35,9 @@ export class CartModalComponent implements OnInit, OnDestroy { public collectionPath: string = ROUTER_UTILS.config.collection.root; public nftPath: string = ROUTER_UTILS.config.nft.root; public cartItemsQuantities: number[] = []; - cartItemPrices: { [key: string]: { originalPrice: number, discountedPrice: number } } = {}; + cartItemPrices: { [key: string]: { originalPrice: number; discountedPrice: number } } = {}; isCartCheckoutOpen = false; - @Output() onCartCheckout = new EventEmitter(); - constructor( public cartService: CartService, private cd: ChangeDetectorRef, @@ -46,11 +45,12 @@ export class CartModalComponent implements OnInit, OnDestroy { private modalService: NzModalService, private nftApi: NftApi, private notification: NzNotificationService, + private router: Router, ) {} ngOnInit() { this.subscriptions.add(this.cartService.getCartItems().subscribe(items => { - //console.log('[CartModalComponent-ngOnInit] Cart items updated:', items); + // console.log('[CartModalComponent-ngOnInit] Cart items updated:', items); this.cartItemsStatus = items.map(item => this.cartItemStatus(item)); this.cartItemsQuantities = items.map(item => this.cartItemSaleAvailableQty(item)); items.forEach(item => { @@ -78,27 +78,27 @@ export class CartModalComponent implements OnInit, OnDestroy { } private refreshCartData() { - //console.log('Refreshing cart items...'); + // console.log('Refreshing cart items...'); const cartItems = this.cartService.getCartItems().getValue(); - //console.log('Current cart items:', cartItems); + // console.log('Current cart items:', cartItems); const freshDataObservables = cartItems.map(item => this.nftApi.getNftById(item.nft.uid).pipe( take(1), map(freshNft => { - //console.log(`Fetched fresh data for NFT ${item.nft.uid}:`, freshNft); + // console.log(`Fetched fresh data for NFT ${item.nft.uid}:`, freshNft); return freshNft ? { ...item, nft: freshNft } : item; }), catchError(error => { - //console.error(`Error fetching fresh data for NFT ${item.nft.uid}:`, error); - return of(item); // Return the original item in case of error + // console.error(`Error fetching fresh data for NFT ${item.nft.uid}:`, error); + return of(item); }) ) ); forkJoin(freshDataObservables).subscribe( freshCartItems => { - //console.log('Fresh cart items:', freshCartItems); + // console.log('Fresh cart items:', freshCartItems); this.cartService.updateCartItems(freshCartItems); @@ -110,7 +110,7 @@ export class CartModalComponent implements OnInit, OnDestroy { this.cartItemPrices[item.nft.uid] = { originalPrice, discountedPrice }; }); - //console.log('Finished refreshing cart items.'); + // console.log('Finished refreshing cart items.'); this.cd.markForCheck(); }, @@ -165,24 +165,24 @@ export class CartModalComponent implements OnInit, OnDestroy { public calcPrice(item: CartItem, discount: number): number { const itemPrice = item.nft?.availablePrice || item.nft?.price || 0; - return this.calc(itemPrice, discount); // assuming calc method applies the discount + return this.calc(itemPrice, discount); } public cartItemStatus(item: CartItem): any { - //console.log("[cart-modal.component-cartItemStatus] function called"); + // console.log("[cart-modal.component-cartItemStatus] function called"); const itemAvailable = this.cartService.isCartItemAvailableForSale(item); if(itemAvailable) { - //console.log("[cart-modal.component-cartItemStatus] returning Available, itemAvailable: " + itemAvailable); - return "Available"; + // console.log("[cart-modal.component-cartItemStatus] returning Available, itemAvailable: " + itemAvailable); + return 'Available'; }; - //console.log("[cart-modal.component-cartItemStatus] returning Not Available, itemAvailable: " + itemAvailable); - return "Not Available"; + // console.log("[cart-modal.component-cartItemStatus] returning Not Available, itemAvailable: " + itemAvailable); + return 'Not Available'; }; private cartItemSaleAvailableQty(item: CartItem): number { - //console.log("[cart-modal.component-cartItemSaleAvailableQty] function called"); + // console.log("[cart-modal.component-cartItemSaleAvailableQty] function called"); const availQty = this.cartService.getAvailableNftQuantity(item); - //console.log("[cart-modal.component] cartItemSaleAvailableQty, qty: " + availQty); + // console.log("[cart-modal.component] cartItemSaleAvailableQty, qty: " + availQty); return availQty; } @@ -190,34 +190,46 @@ export class CartModalComponent implements OnInit, OnDestroy { this.cartService.hideCart(); } - /* - public handleCheckout(): void { - const cartItems = this.cartService.getCartItems().getValue(); - //console.log('Proceeding to checkout with items:', cartItems); + public goToNft(nftUid: string): void { + if (!nftUid) { + console.error('No NFT UID provided.'); + return; + } - this.modalService.create({ - nzTitle: 'Checkout', - nzContent: CheckoutOverlayComponent, - nzComponentParams: { - items: cartItems - }, - nzFooter: null, - nzWidth: '80%' - }); + this.router.navigate(['/', this.nftPath, nftUid]); + this.cartService.hideCart(); + } + + public goToCollection(colUid: string): void { + if (!colUid) { + console.error('No Collection UID provided.'); + return; + } + + this.router.navigate(['/', this.collectionPath, colUid]); + this.cartService.hideCart(); } - */ public handleCartCheckout(): void { const cartItems = this.cartService.getCartItems().getValue(); - this.modalService.create({ + const modalRef = this.modalService.create({ nzTitle: 'Checkout', nzContent: CheckoutOverlayComponent, nzComponentParams: { items: cartItems }, nzFooter: null, nzWidth: '80%', - nzOnOk: () => this.isCartCheckoutOpen = false // Optionally handle the modal close event }); + + modalRef.afterClose.subscribe(() => { + // this.cartService.hideCart(); + }); + } + + public handleCloseCartCheckout(alsoCloseCartModal: boolean): void { + if (alsoCloseCartModal) { + this.cartService.hideCart(); + } } ngOnDestroy() { diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.html b/src/app/components/cart/components/checkout/checkout-overlay.component.html index e6282b2..96e4947 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.html +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.html @@ -9,7 +9,15 @@

Network/Token: {{ group.tokenSy Select this network/group of NFTs for payment - + NFT Name @@ -22,8 +30,16 @@

Network/Token: {{ group.tokenSy - {{ item.nft.name }} - {{ item.collection.name }} + + + {{item.nft.name}} + + + + + {{item.collection.name}} + + {{ item.quantity }} {{ (item.salePrice | formatToken:(item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network):true:true) | async }} @@ -34,12 +50,13 @@

Network/Token: {{ group.tokenSy - Total + + Total {{ group.totalQuantity }} - {{ (group.totalPrice / 1000000) }} {{ group.tokenSymbol }} + {{ (group.totalPrice | formatToken : group.network : true : true) | async }} @@ -69,6 +86,18 @@

Network/Token: {{ group.tokenSy +
+ +
@@ -221,26 +250,12 @@

Network/Token: {{ group.tokenSy nz-button nzType="primary" nzSize="large" - class="w-full lg:mr-2" + class="w-full lg:mr-2 mb-2" i18n (click)="close()" > Close checkout - - - diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.ts b/src/app/components/cart/components/checkout/checkout-overlay.component.ts index 7744df9..aaa32bf 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.ts +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.ts @@ -14,8 +14,8 @@ import { AuthService } from '@components/auth/services/auth.service'; import { NotificationService } from '@core/services/notification'; import { OrderApi } from '@api/order.api'; import { NftApi } from '@api/nft.api'; -//import { FileApi } from '@api/file.api'; -import { BehaviorSubject, firstValueFrom, interval, Subscription, take } from 'rxjs'; +// import { FileApi } from '@api/file.api'; +import { BehaviorSubject, firstValueFrom, interval, Subscription } from 'rxjs'; import { TransactionService } from '@core/services/transaction'; import { getItem, removeItem, setItem, StorageItem } from '@core/utils'; import dayjs from 'dayjs'; @@ -24,6 +24,7 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { ROUTER_UTILS } from '@core/utils/router.utils'; import { Router } from '@angular/router'; import { NzNotificationService } from 'ng-zorro-antd/notification'; +import { NzModalRef } from 'ng-zorro-antd/modal'; export enum StepType { CONFIRM = 'Confirm', @@ -37,6 +38,7 @@ interface GroupedCartItem { items: CartItem[]; totalQuantity: number; totalPrice: number; + network: Network | undefined; } interface HistoryItem { @@ -49,7 +51,7 @@ interface HistoryItem { @UntilDestroy() @Component({ - selector: 'app-checkout-overlay', + selector: 'wen-app-checkout-overlay', templateUrl: './checkout-overlay.component.html', styleUrls: ['./checkout-overlay.component.less'], changeDetection: ChangeDetectionStrategy.OnPush, @@ -59,12 +61,14 @@ export class CheckoutOverlayComponent implements OnInit { @Input() items: CartItem[] = []; @Input() set isOpen(value: boolean) { this._isOpen = value; - //this.checkoutService.modalOpen$.next(value); + // this.checkoutService.modalOpen$.next(value); } + @Output() wenOnClose = new EventEmitter(); + @Output() wenOnCloseCartCheckout = new EventEmitter(); groupedCartItems: GroupedCartItem[] = []; unavailableItemCount = 0; - cartItemPrices: { [key: string]: { originalPrice: number, discountedPrice: number } } = {}; + cartItemPrices: { [key: string]: { originalPrice: number; discountedPrice: number } } = {}; public agreeTermsConditions = false; public transaction$: BehaviorSubject = new BehaviorSubject(undefined); public history: HistoryItem[] = []; @@ -78,9 +82,12 @@ export class CheckoutOverlayComponent implements OnInit { public stepType = StepType; public selectedNetwork: string | null = null; public mintingDataNetwork: Network | undefined; + formattedTotalPrice = ''; + private purchasedTokenSymbol: string | null = null; private transSubscription?: Subscription; public nftPath = ROUTER_UTILS.config.nft.root; + public collectionPath: string = ROUTER_UTILS.config.collection.root; constructor( private cartService: CartService, @@ -91,18 +98,18 @@ export class CheckoutOverlayComponent implements OnInit { public helper: HelperService, private cd: ChangeDetectorRef, private nftApi: NftApi, - //private fileApi: FileApi, private router: Router, private nzNotification: NzNotificationService, + private modalRef: NzModalRef, ) {} ngOnInit() { - console.log('checkout-overlay ngOnInit called, running groupItems code.'); + // console.log('checkout-overlay ngOnInit called, running groupItems code.'); this.groupItems(); this.receivedTransactions = false; const listeningToTransaction: string[] = []; this.transaction$.pipe(untilDestroyed(this)).subscribe((val) => { - console.log('transaction val: ', val); + // console.log('transaction val: ', val); if (val && val.type === TransactionType.ORDER) { this.targetAddress = val.payload.targetAddress; this.targetAmount = val.payload.amount; @@ -168,10 +175,8 @@ export class CheckoutOverlayComponent implements OnInit { this.receivedTransactions = true; this.currentStep = StepType.COMPLETE; - const purchasedItemIds = this.getPurchasedItemIds(val); - this.cartService.removeItemsFromCart(purchasedItemIds); - - + // console.log('[checkout-overlay.component-purchase] transaction after purchase complete: ', val); + this.removePurchasedGroupItems(); this.cd.markForCheck(); }, 2000); @@ -182,7 +187,7 @@ export class CheckoutOverlayComponent implements OnInit { val.payload.nftOrders.forEach(nftOrder => { firstValueFrom(this.nftApi.listen(nftOrder.nft)).then((obj) => { if (obj !== null && obj !== undefined) { - const purchasedNft = obj as Nft; // Type assertion + const purchasedNft = obj as Nft; this.purchasedNfts = [...(this.purchasedNfts || []), purchasedNft]; this.cd.markForCheck(); @@ -190,7 +195,6 @@ export class CheckoutOverlayComponent implements OnInit { }); }); } - } if ( @@ -282,7 +286,7 @@ export class CheckoutOverlayComponent implements OnInit { } groupItems() { - console.log('groupItems function called.') + // console.log('groupItems function called.') const groups: { [tokenSymbol: string]: GroupedCartItem } = {}; this.items.forEach(item => { const tokenSymbol = (item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network) || 'Unknown'; @@ -292,9 +296,17 @@ export class CheckoutOverlayComponent implements OnInit { const price = (this.discount(item) < 1) ? discountedPrice : originalPrice; item.salePrice = price; + const network = (item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network) || undefined; + if (this.cartService.isCartItemAvailableForSale(item)) { if (!groups[tokenSymbol]) { - groups[tokenSymbol] = { tokenSymbol, items: [], totalQuantity: 0, totalPrice: 0 }; + groups[tokenSymbol] = { + tokenSymbol, + items: [], + totalQuantity: 0, + totalPrice: 0, + network + }; } groups[tokenSymbol].items.push(item); groups[tokenSymbol].totalQuantity += item.quantity; @@ -303,10 +315,21 @@ export class CheckoutOverlayComponent implements OnInit { this.unavailableItemCount++; } - console.log('Cart item loop finished, group: ', groups[tokenSymbol]) + // console.log('Cart item loop finished, group: ', groups[tokenSymbol]) }); this.groupedCartItems = Object.values(groups); + + if (this.groupedCartItems.length === 1) { + this.selectedNetwork = this.groupedCartItems[0].tokenSymbol; + } + } + + private removePurchasedGroupItems(): void { + if (this.purchasedTokenSymbol) { + this.cartService.removeGroupItemsFromCart(this.purchasedTokenSymbol); + this.purchasedTokenSymbol = null; + } } private calcPrice(item: CartItem, discount: number): number { @@ -329,17 +352,33 @@ export class CheckoutOverlayComponent implements OnInit { this.cd.markForCheck(); } - public close(): void { - this.reset(); - this.wenOnClose.next(); + public close(alsoCloseCartModal = false): void { + this.wenOnCloseCartCheckout.emit(alsoCloseCartModal); + this.modalRef.close(); } - public getRecords(): Nft[] | null | undefined { - return this.purchasedNfts || null; + public goToNft(nftUid: string, alsoCloseCartModal = false): void { + if (!nftUid) { + console.error('No NFT UID provided.'); + return; + } + this.router.navigate(['/', this.nftPath, nftUid]); + this.wenOnCloseCartCheckout.emit(alsoCloseCartModal); + this.modalRef.close(); } - private getPurchasedItemIds(transaction: Transaction): string[] { - return transaction.payload.nftOrders?.map(item => item.nft) || []; + public goToCollection(colUid: string, alsoCloseCartModal = false): void { + if (!colUid) { + console.error('No Collection UID provided.'); + return; + } + this.router.navigate(['/', this.collectionPath, colUid]); + this.wenOnCloseCartCheckout.emit(alsoCloseCartModal); + this.modalRef.close(); + } + + public getRecords(): Nft[] | null | undefined { + return this.purchasedNfts || null; } public pushToHistory( @@ -376,6 +415,7 @@ export class CheckoutOverlayComponent implements OnInit { const selectedGroup = this.groupedCartItems.find(group => group.tokenSymbol === this.selectedNetwork); if (selectedGroup && selectedGroup.items.length > 0) { + this.purchasedTokenSymbol = selectedGroup.tokenSymbol; const firstItem = selectedGroup.items[0]; this.mintingDataNetwork = firstItem.nft?.placeholderNft ? firstItem.collection?.mintingData?.network : firstItem.nft?.mintingData?.network; } @@ -386,7 +426,6 @@ export class CheckoutOverlayComponent implements OnInit { return; } - // Convert only the NFTs from the selected group const nfts = this.convertGroupedCartItemsToNfts(selectedGroup); if (nfts.length === 0) { @@ -398,26 +437,26 @@ export class CheckoutOverlayComponent implements OnInit { await this.proceedWithBulkOrder(nfts); } - - convertGroupedCartItemsToNfts(selectedGroup: GroupedCartItem): NftPurchaseRequest[] { + public convertGroupedCartItemsToNfts(selectedGroup: GroupedCartItem): NftPurchaseRequest[] { const nfts: NftPurchaseRequest[] = []; selectedGroup.items.forEach(item => { if (item.nft && item.collection) { - const nftData: NftPurchaseRequest = { - collection: item.collection.uid, - }; + // console.log('[checkout-overlay.component-convertGroupedCartItemsToNfts] looped nft (item): ', item); - if (item.collection.type === CollectionType.CLASSIC) { - nftData.nft = item.nft.uid; - } + for (let i = 0; i < item.quantity; i++) { + // console.log('[checkout-overlay.component-convertGroupedCartItemsToNfts] creating bulk order NftPurchaseRequest for item, qty#: ', i) + const nftData: NftPurchaseRequest = { + collection: item.collection.uid, + }; - // If owner is set, CollectionType is not relevant. - if (item.nft.owner) { - nftData.nft = item.nft.uid; - } + if (item.nft.owner || item.collection.type === CollectionType.CLASSIC) { + // console.log('[checkout-overlay.component-convertGroupedCartItemsToNfts] passed item.nft.owner || item.collection.type === CollectionType.CLASSIC. item.nft.owner: ' + item.nft.owner + '. item.collection.type: ' + item.collection.type + '. item.nft.uid: ' + item.nft.uid); + nftData.nft = item.nft.uid; + } - nfts.push(nftData); + nfts.push(nftData); + } } }); @@ -425,6 +464,7 @@ export class CheckoutOverlayComponent implements OnInit { } public async proceedWithBulkOrder(nfts: NftPurchaseRequest[]): Promise { + // console.log('[checkout-overlay.component-proceddWithBulkOrder] nfts passed in: ', nfts) const selectedGroup = this.groupedCartItems.find(group => group.tokenSymbol === this.selectedNetwork); if (!selectedGroup) { console.warn('No network selected or no items in the selected network.'); @@ -442,6 +482,8 @@ export class CheckoutOverlayComponent implements OnInit { orders: nfts }; + // console.log('[checkout-overlay.component-proceddWithBulkOrder] params being passed for signing: ', bulkPurchaseRequest); + await this.auth.sign(bulkPurchaseRequest, async (signedRequest, finish) => { this.notification .processRequest(this.orderApi.orderNfts(signedRequest), $localize`Bulk order created.`, finish) @@ -458,5 +500,4 @@ export class CheckoutOverlayComponent implements OnInit { }); }); } - } diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts index 00db18f..ff52cf1 100644 --- a/src/app/components/cart/services/cart.service.ts +++ b/src/app/components/cart/services/cart.service.ts @@ -31,7 +31,7 @@ export class CartService { private helperService: HelperService, public auth: AuthService, ) { - //console.log('CartService instance created'); + // console.log('CartService instance created'); } public showCart(): void { @@ -47,7 +47,7 @@ export class CartService { } public addToCart(cartItem: CartItem): void { - //console.log('[CartService] addToCart function called.'); + console.log('[CartService] addToCart function called. Adding cart item: ', cartItem); const currentItems = this.cartItemsSubject.value; const isItemAlreadyInCart = currentItems.some(item => item.nft.uid === cartItem.nft.uid); @@ -57,19 +57,19 @@ export class CartService { this.cartItemsSubject.next(updatedCartItems); this.saveCartItems(); this.notification.success($localize`NFT ` + cartItem.nft.name + ` from collection ` + cartItem.collection.name + ` has been added to your cart.`, ''); - //console.log('[CartService] NFT added to cart:', cartItem); + // console.log('[CartService] NFT added to cart:', cartItem); } else { - //console.log('[CartService] NFT is already in the cart:', cartItem); + // console.log('[CartService] NFT is already in the cart:', cartItem); this.notification.error($localize`This NFT already exists in your cart.`, ''); } } public removeFromCart(itemId: string): void { - //console.log('[CartService] removeFromCart function called.'); + // console.log('[CartService] removeFromCart function called.'); const updatedCartItems = this.cartItemsSubject.value.filter(item => item.nft.uid !== itemId); this.cartItemsSubject.next(updatedCartItems); this.saveCartItems(); - //console.log('[CartService-removeFromCart] Cart updated:', updatedCartItems); + // console.log('[CartService-removeFromCart] Cart updated:', updatedCartItems); } public removeItemsFromCart(itemIds: string[]): void { @@ -78,8 +78,17 @@ export class CartService { this.saveCartItems(); } + public removeGroupItemsFromCart(tokenSymbol: string): void { + const updatedCartItems = this.cartItemsSubject.value.filter(item => { + const itemTokenSymbol = (item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network) || 'Unknown'; + return itemTokenSymbol !== tokenSymbol; + }); + this.cartItemsSubject.next(updatedCartItems); + this.saveCartItems(); + } + public getCartItems(): BehaviorSubject { - //console.log('[CartService] getCartItems function called.'); + // console.log('[CartService] getCartItems function called.'); return this.cartItemsSubject; } @@ -89,15 +98,15 @@ export class CartService { } public saveCartItems(): void { - //console.log('[CartService] getCartItems function called.'); + // console.log('[CartService] getCartItems function called.'); setItem(StorageItem.CartItems, this.cartItemsSubject.value); - //console.log('[CartService] Saving cart items to local storage:', this.cartItemsSubject.value); + // console.log('[CartService] Saving cart items to local storage:', this.cartItemsSubject.value); } private loadCartItems(): CartItem[] { - //console.log('[CartService] Loading cart items from local storage'); + // console.log('[CartService] Loading cart items from local storage'); const items = getItem(StorageItem.CartItems) as CartItem[]; - //console.log('[CartService] Cart items loaded from local storage:', items); + // console.log('[CartService] Cart items loaded from local storage:', items); return items || []; } @@ -105,7 +114,7 @@ export class CartService { const isLocked = this.helperService.isLocked(nft, collection, true); const isOwner = nft.owner === this.auth.member$.value?.uid; const availableForSale = this.helperService.isAvailableForSale(nft, collection); - //console.log(`[cart.service-isNftAvailableForSale] results for NFT ${nft.name}; availableForSale: ${availableForSale}, isLocked: ${isLocked}, isOwner: ${isOwner}`); + // console.log(`[cart.service-isNftAvailableForSale] results for NFT ${nft.name}; availableForSale: ${availableForSale}, isLocked: ${isLocked}, isOwner: ${isOwner}`); return !isLocked && availableForSale && (!isOwner || !nft.owner); } @@ -116,16 +125,16 @@ export class CartService { public getAvailableNftQuantity(cartItem: CartItem): number { const isAvailableForSale = this.helperService.isAvailableForSale(cartItem.nft, cartItem.collection); - //console.log("[cart.service-getAvailableNftQuantity] function called for cartItem.nft.name: " + cartItem.nft.name + ", isAvailableForSale: " + isAvailableForSale); + // console.log("[cart.service-getAvailableNftQuantity] function called for cartItem.nft.name: " + cartItem.nft.name + ", isAvailableForSale: " + isAvailableForSale); if (cartItem.nft.placeholderNft && isAvailableForSale) { - //console.log("[service-getAvailableNftQuantity] returning qty, placeholder: " + cartItem.nft.placeholderNft + ". availableNfts " + cartItem.collection.availableNfts); + // console.log("[service-getAvailableNftQuantity] returning qty, placeholder: " + cartItem.nft.placeholderNft + ". availableNfts " + cartItem.collection.availableNfts); return cartItem.collection.availableNfts || 0; } else if (isAvailableForSale) { - //console.log("[service-getAvailableNftQuantity] returning 1, isAvailableForSale: " + isAvailableForSale + ". placeholder: " + cartItem.nft.placeholderNft); + // console.log("[service-getAvailableNftQuantity] returning 1, isAvailableForSale: " + isAvailableForSale + ". placeholder: " + cartItem.nft.placeholderNft); return 1; } - //console.log("[service-getAvailableNftQuantity] returning 0, isAvailableForSale: " + isAvailableForSale + ". placeholder: " + cartItem.nft.placeholderNft); + // console.log("[service-getAvailableNftQuantity] returning 0, isAvailableForSale: " + isAvailableForSale + ". placeholder: " + cartItem.nft.placeholderNft); return 0; } 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..e2568a3 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 @@ -432,10 +432,13 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { params.nft = this.nft.uid; } + console.log('[nft-checkout.component-proceedWithOrder] params being passed for signing: ', params); + await this.auth.sign(params, (sc, finish) => { this.notification .processRequest(this.orderApi.orderNft(sc), $localize`Order created.`, finish) .subscribe((val: any) => { + console.log('[nft-this.checkoutService.component-proceedWithOrder] val after processing: ', val); this.transSubscription?.unsubscribe(); setItem(StorageItem.CheckoutTransaction, val.uid); this.transSubscription = this.orderApi.listen(val.uid).subscribe(this.transaction$); diff --git a/src/app/pages/award/pages/new/new.page.ts b/src/app/pages/award/pages/new/new.page.ts index c8a5f8d..f0ef978 100644 --- a/src/app/pages/award/pages/new/new.page.ts +++ b/src/app/pages/award/pages/new/new.page.ts @@ -17,7 +17,7 @@ import { TEST_AVAILABLE_MINTABLE_NETWORKS, Token, TokenStatus, -} from '@build-5/interfaces'; + getDefDecimalIfNotSet, Network } from '@build-5/interfaces'; import { BehaviorSubject, of, Subscription, switchMap } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { AwardApi } from './../../../../@api/award.api'; @@ -27,7 +27,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/collection/pages/collection/nfts/nfts.page.ts b/src/app/pages/collection/pages/collection/nfts/nfts.page.ts index c29badf..136561b 100644 --- a/src/app/pages/collection/pages/collection/nfts/nfts.page.ts +++ b/src/app/pages/collection/pages/collection/nfts/nfts.page.ts @@ -5,6 +5,7 @@ import { Input, OnChanges, OnInit, + OnDestroy, } from '@angular/core'; import { NftApi } from '@api/nft.api'; import { CollectionApi } from '@api/collection.api'; @@ -18,14 +19,14 @@ import { FilterStorageService } from '@core/services/filter-storage'; import { UntilDestroy } from '@ngneat/until-destroy'; import { marketSections } from '@pages/market/pages/market/market.page'; import { FilterService } from '@pages/market/services/filter.service'; -import { COL, Timestamp } from '@build-5/interfaces'; +import { COL, Timestamp, Collection } from '@build-5/interfaces'; import { InstantSearchConfig } from 'angular-instantsearch/instantsearch/instantsearch'; import { Subject, take, filter, takeUntil } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { CartService } from '@components/cart/services/cart.service'; import { NzNotificationService } from 'ng-zorro-antd/notification'; import { state } from '@angular/animations'; -import { Collection } from '@build-5/interfaces'; + import { CollectionNftStateService } from './collectionNfts.service'; // used in src/app/pages/collection/pages/collection/collection.page.ts @@ -48,7 +49,7 @@ export enum HOT_TAGS { // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection changeDetection: ChangeDetectionStrategy.Default, }) -export class CollectionNFTsPage implements OnInit, OnChanges { +export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { @Input() public collectionId?: string | null; config?: InstantSearchConfig; sections = marketSections; @@ -91,7 +92,7 @@ export class CollectionNFTsPage implements OnInit, OnChanges { } }, error: err => { - //console.error('Error fetching collection:', err); + // console.error('Error fetching collection:', err); this.notification.error($localize`Error occurred while fetching collection.`, ''); } }); @@ -134,7 +135,6 @@ export class CollectionNFTsPage implements OnInit, OnChanges { if (hits && hits.length > 0 && this.collection) { this.originalNfts = hits; this.collectionNftStateService.setListedNfts(hits, this.collection); - } else { } } @@ -197,8 +197,6 @@ export class CollectionNFTsPage implements OnInit, OnChanges { takeUntil(this.destroy$) ) .subscribe({ - next: nftsToAdd => { - }, error: err => { this.notification.error($localize`Error occurred while fetching collection.`, ''); } diff --git a/src/app/pages/nft/pages/nft/nft.page.html b/src/app/pages/nft/pages/nft/nft.page.html index aeed9e9..f77166b 100644 --- a/src/app/pages/nft/pages/nft/nft.page.html +++ b/src/app/pages/nft/pages/nft/nft.page.html @@ -4,8 +4,21 @@ NFT + > + NFT + + + {{ getCollectionTypeString(collectionType) }} + + + {{ getCollectionStatusString(collectionMinting) }} + { - //console.log('[OnInit] Current NFT:', nft); + // console.log('[OnInit] Current NFT:', nft); this.currentNft = nft; }); this.deviceService.viewWithSearch$.next(false); @@ -275,6 +278,9 @@ export class NFTPage implements OnInit, OnDestroy { this.data.collection$.pipe(skip(1), untilDestroyed(this)).subscribe(async (p) => { if (p) { + this.collectionType = p.type; + // this.collectionMinting = p.status; + // console.log('[nft.page-p] collectionMinting set to: ', p.status) this.collectionSubscriptions$.forEach((s) => { s.unsubscribe(); }); @@ -401,6 +407,21 @@ export class NFTPage implements OnInit, OnDestroy { } } + public getCollectionTypeString(type: CollectionType | null | undefined): string { + if (type === null || type === undefined) { + return 'Unknown'; + } + return CollectionType[type]; + } + + public getCollectionStatusString(status: CollectionStatus | null | undefined): string { + if (status === null || status === undefined) { + return 'Unknown'; + } + console.log('[nft.page-getCollectionStatusString] return collection status: ', CollectionStatus[status]); + return CollectionStatus[status]; + } + private listenToNft(id: string): void { this.cancelSubscriptions(); this.data.nftId = id; @@ -532,18 +553,18 @@ export class NFTPage implements OnInit, OnDestroy { public addToCart(nft: Nft): void { if (nft && this.data.collection$) { - //console.log('[addToCart-this.data.collection$.value', this.data.collection$.value) - //console.log('[addToCart-this.data.nft$.value', this.data.nft$.value) + // console.log('[addToCart-this.data.collection$.value', this.data.collection$.value) + // console.log('[addToCart-this.data.nft$.value', this.data.nft$.value) this.data.collection$.pipe(take(1)).subscribe(collection => { if (collection) { this.cartService.addToCart({ nft, collection, quantity: 1, salePrice: 0 }); - //console.log('Added to cart:', nft, collection); + // console.log('Added to cart:', nft, collection); } else { - //console.error('Collection is undefined or null'); + // console.error('Collection is undefined or null'); } }); } else { - //console.error('NFT is undefined or null'); + // console.error('NFT is undefined or null'); } } diff --git a/src/app/pages/nft/services/helper.service.ts b/src/app/pages/nft/services/helper.service.ts index 83553bf..3a6b75f 100644 --- a/src/app/pages/nft/services/helper.service.ts +++ b/src/app/pages/nft/services/helper.service.ts @@ -124,21 +124,21 @@ export class HelperService { } public getDate(date: any): any { - //console.log(`[getDate] Original input:`, date); + // console.log(`[getDate] Original input:`, date); if (typeof date === 'object') { if (date?.toDate) { const dateFromObject = date.toDate(); - //console.log(`[getDate] Object with toDate method detected, toDate result:`, dateFromObject); + // console.log(`[getDate] Object with toDate method detected, toDate result:`, dateFromObject); return dateFromObject; } else if (date?.seconds) { const dateFromSeconds = new Date(date.seconds * 1000); // Convert to milliseconds - //console.log(`[getDate] Object with seconds property detected, converted to Date:`, dateFromSeconds); + // console.log(`[getDate] Object with seconds property detected, converted to Date:`, dateFromSeconds); return dateFromSeconds; } } - //console.log(`[getDate] Returning undefined, input could not be parsed as a date.`); + // console.log(`[getDate] Returning undefined, input could not be parsed as a date.`); return undefined; } @@ -174,9 +174,9 @@ export class HelperService { } public isAvailableForSale(nft?: Nft | null, col?: Collection | null): boolean { - //console.log("[NFThelper-isAvailableForSale] function called"); + // console.log("[NFThelper-isAvailableForSale] function called"); if (!col || !nft?.availableFrom || col?.status === CollectionStatus.MINTING) { - //console.log("[NFT helper.service.ts] isAvailableForSale function returning false. nft name: " + nft?.name + ", col name: " + col?.name) + // console.log("[NFT helper.service.ts] isAvailableForSale function returning false. nft name: " + nft?.name + ", col name: " + col?.name) return false; } @@ -186,10 +186,10 @@ export class HelperService { !!this.getDate(nft.availableFrom) && dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's') ); - //console.log("[NFT helper.service.ts] isAvailableForSale function returning " + isAvail + ". nft name: " + nft?.name + ", col name: " + col?.name + ". nft.availableFrom: " + nft.availableFrom.seconds); - //console.log("col.approved: " + (col.approved === true)); - //console.log("!!this.getDate(nft.availableFrom): " + !!this.getDate(nft.availableFrom) + ", this.getDate(nft.availableFrom): " + this.getDate(nft.availableFrom)); - //console.log("dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's')" + dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's')); + // console.log("[NFT helper.service.ts] isAvailableForSale function returning " + isAvail + ". nft name: " + nft?.name + ", col name: " + col?.name + ". nft.availableFrom: " + nft.availableFrom.seconds); + // console.log("col.approved: " + (col.approved === true)); + // console.log("!!this.getDate(nft.availableFrom): " + !!this.getDate(nft.availableFrom) + ", this.getDate(nft.availableFrom): " + this.getDate(nft.availableFrom)); + // console.log("dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's')" + dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's')); return isAvail; } From 0eda115d2b2bb852f199498f7967ec91b7204e25 Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Wed, 24 Jan 2024 17:13:49 -0500 Subject: [PATCH 04/24] Prep for PR --- src/app/@api/order.api.ts | 5 +- .../@shell/ui/header/header.component.html | 18 +++- src/app/@shell/ui/header/header.component.ts | 17 ++- src/app/components/cart/cart.module.ts | 10 +- .../cart-modal/cart-modal.component.html | 100 ++++++++++++++---- .../cart-modal/cart-modal.component.ts | 77 +++++++------- .../checkout/checkout-overlay.component.html | 59 ++++++++--- .../checkout/checkout-overlay.component.ts | 95 +++++++++++++---- .../components/cart/services/cart.service.ts | 38 ++++--- .../nft-card/nft-card.component.html | 6 +- .../components/nft-card/nft-card.component.ts | 7 +- .../nft-checkout/nft-checkout.component.ts | 10 +- src/app/pages/award/pages/new/new.page.ts | 4 +- .../collection/nfts/collectionNfts.service.ts | 12 +-- .../pages/collection/nfts/nfts.page.html | 19 +++- .../pages/collection/nfts/nfts.page.ts | 43 ++++---- src/app/pages/nft/pages/nft/nft.page.html | 8 +- src/app/pages/nft/pages/nft/nft.page.ts | 9 +- src/app/pages/nft/services/helper.service.ts | 8 +- 19 files changed, 364 insertions(+), 181 deletions(-) diff --git a/src/app/@api/order.api.ts b/src/app/@api/order.api.ts index 414048a..71784d7 100644 --- a/src/app/@api/order.api.ts +++ b/src/app/@api/order.api.ts @@ -28,8 +28,9 @@ 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 orderNfts = ( + req: Build5Request, + ): Observable => this.request(WEN_FUNC.orderNftBulk, req); public orderToken = ( req: Build5Request, diff --git a/src/app/@shell/ui/header/header.component.html b/src/app/@shell/ui/header/header.component.html index 736b958..d6222f3 100644 --- a/src/app/@shell/ui/header/header.component.html +++ b/src/app/@shell/ui/header/header.component.html @@ -51,9 +51,14 @@ (click)="openShoppingCart()" > - + - +

-
Price Each
+
+ Price Each +
-
- {{ (cartItemPrices[item.nft.uid]?.originalPrice | formatToken:(item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network):true:true) | async }} +
+ {{ + cartItemPrices[item.nft.uid]?.originalPrice + | formatToken + : (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) + : true + : true + | async + }}
- {{ (cartItemPrices[item.nft.uid]?.discountedPrice | formatToken:(item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network):true:true) | async }} + {{ + cartItemPrices[item.nft.uid]?.discountedPrice + | formatToken + : (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) + : true + : true + | async + }}
@@ -82,7 +129,9 @@
-
Remove
+
+ Remove +

- -
diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.ts b/src/app/components/cart/components/cart-modal/cart-modal.component.ts index c79caa0..cb354c4 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.ts +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.ts @@ -8,11 +8,7 @@ import { EventEmitter, Output, } from '@angular/core'; -import { - Nft, - Collection, - MIN_AMOUNT_TO_TRANSFER, - } from '@build-5/interfaces'; +import { Nft, Collection, MIN_AMOUNT_TO_TRANSFER } from '@build-5/interfaces'; import { Subscription, forkJoin, map, take, catchError, of } from 'rxjs'; import { CartService, CartItem } from './../../services/cart.service'; import { AuthService } from '@components/auth/services/auth.service'; @@ -23,12 +19,11 @@ import { CheckoutOverlayComponent } from '../checkout/checkout-overlay.component import { NftApi } from '@api/nft.api'; import { NzNotificationService } from 'ng-zorro-antd/notification'; - @Component({ selector: 'wen-app-cart-modal', templateUrl: './cart-modal.component.html', styleUrls: ['./cart-modal.component.less'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CartModalComponent implements OnInit, OnDestroy { private subscriptions = new Subscription(); @@ -49,22 +44,26 @@ export class CartModalComponent implements OnInit, OnDestroy { ) {} ngOnInit() { - this.subscriptions.add(this.cartService.getCartItems().subscribe(items => { - // console.log('[CartModalComponent-ngOnInit] Cart items updated:', items); - this.cartItemsStatus = items.map(item => this.cartItemStatus(item)); - this.cartItemsQuantities = items.map(item => this.cartItemSaleAvailableQty(item)); - items.forEach(item => { - const originalPrice = this.calcPrice(item, 1); - const discountedPrice = this.calcPrice(item, this.discount(item.collection, item.nft)); - this.cartItemPrices[item.nft.uid] = { originalPrice, discountedPrice }; - }); - })); - - this.subscriptions.add(this.cartService.showCart$.subscribe(show => { - if (show) { - this.refreshCartData(); - } - })); + this.subscriptions.add( + this.cartService.getCartItems().subscribe((items) => { + // console.log('[CartModalComponent-ngOnInit] Cart items updated:', items); + this.cartItemsStatus = items.map((item) => this.cartItemStatus(item)); + this.cartItemsQuantities = items.map((item) => this.cartItemSaleAvailableQty(item)); + items.forEach((item) => { + const originalPrice = this.calcPrice(item, 1); + const discountedPrice = this.calcPrice(item, this.discount(item.collection, item.nft)); + this.cartItemPrices[item.nft.uid] = { originalPrice, discountedPrice }; + }); + }), + ); + + this.subscriptions.add( + this.cartService.showCart$.subscribe((show) => { + if (show) { + this.refreshCartData(); + } + }), + ); } cartItemsStatus: string[] = []; @@ -82,29 +81,31 @@ export class CartModalComponent implements OnInit, OnDestroy { const cartItems = this.cartService.getCartItems().getValue(); // console.log('Current cart items:', cartItems); - const freshDataObservables = cartItems.map(item => + const freshDataObservables = cartItems.map((item) => this.nftApi.getNftById(item.nft.uid).pipe( take(1), - map(freshNft => { + map((freshNft) => { // console.log(`Fetched fresh data for NFT ${item.nft.uid}:`, freshNft); return freshNft ? { ...item, nft: freshNft } : item; }), - catchError(error => { + catchError((error) => { // console.error(`Error fetching fresh data for NFT ${item.nft.uid}:`, error); return of(item); - }) - ) + }), + ), ); forkJoin(freshDataObservables).subscribe( - freshCartItems => { + (freshCartItems) => { // console.log('Fresh cart items:', freshCartItems); this.cartService.updateCartItems(freshCartItems); - this.cartItemsStatus = freshCartItems.map(item => this.cartItemStatus(item)); - this.cartItemsQuantities = freshCartItems.map(item => this.cartItemSaleAvailableQty(item)); - freshCartItems.forEach(item => { + this.cartItemsStatus = freshCartItems.map((item) => this.cartItemStatus(item)); + this.cartItemsQuantities = freshCartItems.map((item) => + this.cartItemSaleAvailableQty(item), + ); + freshCartItems.forEach((item) => { const originalPrice = this.calcPrice(item, 1); const discountedPrice = this.calcPrice(item, this.discount(item.collection, item.nft)); this.cartItemPrices[item.nft.uid] = { originalPrice, discountedPrice }; @@ -114,10 +115,10 @@ export class CartModalComponent implements OnInit, OnDestroy { this.cd.markForCheck(); }, - error => { + (error) => { console.error('Error while refreshing cart items: ', error); this.notification.error($localize`Error while refreshing cart items: ` + error, ''); - } + }, ); } @@ -129,7 +130,7 @@ export class CartModalComponent implements OnInit, OnDestroy { this.cartService.removeFromCart(itemId); } else { const cartItems = this.cartService.getCartItems().getValue(); - const itemIndex = cartItems.findIndex(cartItem => cartItem.nft.uid === itemId); + const itemIndex = cartItems.findIndex((cartItem) => cartItem.nft.uid === itemId); if (itemIndex !== -1) { cartItems[itemIndex].quantity = newQuantity; this.cartService.saveCartItems(); @@ -171,13 +172,13 @@ export class CartModalComponent implements OnInit, OnDestroy { public cartItemStatus(item: CartItem): any { // console.log("[cart-modal.component-cartItemStatus] function called"); const itemAvailable = this.cartService.isCartItemAvailableForSale(item); - if(itemAvailable) { + if (itemAvailable) { // console.log("[cart-modal.component-cartItemStatus] returning Available, itemAvailable: " + itemAvailable); return 'Available'; - }; + } // console.log("[cart-modal.component-cartItemStatus] returning Not Available, itemAvailable: " + itemAvailable); return 'Not Available'; - }; + } private cartItemSaleAvailableQty(item: CartItem): number { // console.log("[cart-modal.component-cartItemSaleAvailableQty] function called"); diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.html b/src/app/components/cart/components/checkout/checkout-overlay.component.html index 96e4947..a1be5fd 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.html +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.html @@ -1,12 +1,18 @@
- Notice {{ unavailableItemCount }} items were not included in the checkout due to not being available for sale. + Notice {{ unavailableItemCount }} items were not included in + the checkout due to not being available for sale.

Network/Token: {{ group.tokenSymbol }}

Network/Token: {{ group.tokenSy nzSize="small" [ngClass]="{ 'table-dark': (themeService.theme$ | async) === themes.Dark, - 'table-light': (themeService.theme$ | async) === themes.Light}" + 'table-light': (themeService.theme$ | async) === themes.Light + }" > @@ -31,32 +38,58 @@

Network/Token: {{ group.tokenSy - - {{item.nft.name}} + + {{ item.nft.name }} - - {{item.collection.name}} + + {{ item.collection.name }} {{ item.quantity }} - {{ (item.salePrice | formatToken:(item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network):true:true) | async }} + {{ + item.salePrice + | formatToken + : (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) + : true + : true + | async + }} - {{ ((item.quantity * item.salePrice) | formatToken:(item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network):true:true) | async }} + {{ + item.quantity * item.salePrice + | formatToken + : (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) + : true + : true + | async + }} Total - {{ group.totalQuantity }} + + {{ group.totalQuantity }} + - {{ (group.totalPrice | formatToken : group.network : true : true) | async }} + {{ group.totalPrice | formatToken : group.network : true : true | async }} @@ -161,9 +194,7 @@

Network/Token: {{ group.tokenSy class="block w-full" *ngIf="!helper.isExpired(transaction$ | async)" [targetAddress]="targetAddress" - [formattedAmount]=" - targetAmount | formatToken : mintingDataNetwork : true | async - " + [formattedAmount]="targetAmount | formatToken : mintingDataNetwork : true | async" > diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.ts b/src/app/components/cart/components/checkout/checkout-overlay.component.ts index aaa32bf..a1ded8b 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.ts +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.ts @@ -1,4 +1,12 @@ -import { Component, Input, OnInit, ChangeDetectorRef, ChangeDetectionStrategy, Output, EventEmitter, } from '@angular/core'; +import { + Component, + Input, + OnInit, + ChangeDetectorRef, + ChangeDetectionStrategy, + Output, + EventEmitter, +} from '@angular/core'; import { CartItem, CartService } from './../../services/cart.service'; import { CollectionType, @@ -25,6 +33,7 @@ import { ROUTER_UTILS } from '@core/utils/router.utils'; import { Router } from '@angular/router'; import { NzNotificationService } from 'ng-zorro-antd/notification'; import { NzModalRef } from 'ng-zorro-antd/modal'; +import { ThemeList, ThemeService } from '@core/services/theme'; export enum StepType { CONFIRM = 'Confirm', @@ -70,9 +79,12 @@ export class CheckoutOverlayComponent implements OnInit { unavailableItemCount = 0; cartItemPrices: { [key: string]: { originalPrice: number; discountedPrice: number } } = {}; public agreeTermsConditions = false; - public transaction$: BehaviorSubject = new BehaviorSubject(undefined); + public transaction$: BehaviorSubject = new BehaviorSubject< + Transaction | undefined + >(undefined); public history: HistoryItem[] = []; - public expiryTicker$: BehaviorSubject = new BehaviorSubject(null); + public expiryTicker$: BehaviorSubject = + new BehaviorSubject(null); public invalidPayment = false; public receivedTransactions = false; public targetAddress?: string; @@ -89,6 +101,7 @@ export class CheckoutOverlayComponent implements OnInit { public nftPath = ROUTER_UTILS.config.nft.root; public collectionPath: string = ROUTER_UTILS.config.collection.root; + public theme = ThemeList; constructor( private cartService: CartService, private auth: AuthService, @@ -101,8 +114,13 @@ export class CheckoutOverlayComponent implements OnInit { private router: Router, private nzNotification: NzNotificationService, private modalRef: NzModalRef, + public themeService: ThemeService, ) {} + public get themes(): typeof ThemeList { + return ThemeList; + } + ngOnInit() { // console.log('checkout-overlay ngOnInit called, running groupItems code.'); this.groupItems(); @@ -184,7 +202,7 @@ export class CheckoutOverlayComponent implements OnInit { // Load purchased NFTs. if (val.payload.nftOrders && val.payload.nftOrders.length > 0) { this.purchasedNfts = this.purchasedNfts || []; - val.payload.nftOrders.forEach(nftOrder => { + val.payload.nftOrders.forEach((nftOrder) => { firstValueFrom(this.nftApi.listen(nftOrder.nft)).then((obj) => { if (obj !== null && obj !== undefined) { const purchasedNft = obj as Nft; @@ -288,15 +306,21 @@ export class CheckoutOverlayComponent implements OnInit { groupItems() { // console.log('groupItems function called.') const groups: { [tokenSymbol: string]: GroupedCartItem } = {}; - this.items.forEach(item => { - const tokenSymbol = (item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network) || 'Unknown'; + this.items.forEach((item) => { + const tokenSymbol = + (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) || 'Unknown'; const discount = this.discount(item); const originalPrice = this.calcPrice(item, 1); const discountedPrice = this.calcPrice(item, discount); - const price = (this.discount(item) < 1) ? discountedPrice : originalPrice; + const price = this.discount(item) < 1 ? discountedPrice : originalPrice; item.salePrice = price; - const network = (item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network) || undefined; + const network = + (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) || undefined; if (this.cartService.isCartItemAvailableForSale(item)) { if (!groups[tokenSymbol]) { @@ -305,7 +329,7 @@ export class CheckoutOverlayComponent implements OnInit { items: [], totalQuantity: 0, totalPrice: 0, - network + network, }; } groups[tokenSymbol].items.push(item); @@ -412,17 +436,24 @@ export class CheckoutOverlayComponent implements OnInit { } public async initiateBulkOrder(): Promise { - const selectedGroup = this.groupedCartItems.find(group => group.tokenSymbol === this.selectedNetwork); + const selectedGroup = this.groupedCartItems.find( + (group) => group.tokenSymbol === this.selectedNetwork, + ); if (selectedGroup && selectedGroup.items.length > 0) { this.purchasedTokenSymbol = selectedGroup.tokenSymbol; const firstItem = selectedGroup.items[0]; - this.mintingDataNetwork = firstItem.nft?.placeholderNft ? firstItem.collection?.mintingData?.network : firstItem.nft?.mintingData?.network; + this.mintingDataNetwork = firstItem.nft?.placeholderNft + ? firstItem.collection?.mintingData?.network + : firstItem.nft?.mintingData?.network; } if (!selectedGroup || selectedGroup.items.length === 0) { console.warn('No network selected or no items in the selected network.'); - this.nzNotification.error($localize`No network selected or no items in the selected network.`, ''); + this.nzNotification.error( + $localize`No network selected or no items in the selected network.`, + '', + ); return; } @@ -440,7 +471,7 @@ export class CheckoutOverlayComponent implements OnInit { public convertGroupedCartItemsToNfts(selectedGroup: GroupedCartItem): NftPurchaseRequest[] { const nfts: NftPurchaseRequest[] = []; - selectedGroup.items.forEach(item => { + selectedGroup.items.forEach((item) => { if (item.nft && item.collection) { // console.log('[checkout-overlay.component-convertGroupedCartItemsToNfts] looped nft (item): ', item); @@ -465,37 +496,59 @@ export class CheckoutOverlayComponent implements OnInit { public async proceedWithBulkOrder(nfts: NftPurchaseRequest[]): Promise { // console.log('[checkout-overlay.component-proceddWithBulkOrder] nfts passed in: ', nfts) - const selectedGroup = this.groupedCartItems.find(group => group.tokenSymbol === this.selectedNetwork); + const selectedGroup = this.groupedCartItems.find( + (group) => group.tokenSymbol === this.selectedNetwork, + ); if (!selectedGroup) { console.warn('No network selected or no items in the selected network.'); - this.nzNotification.error($localize`No network selected or no items in the selected network.`, ''); + this.nzNotification.error( + $localize`No network selected or no items in the selected network.`, + '', + ); return; } if (nfts.length === 0 || !this.agreeTermsConditions) { console.warn('No NFTs to purchase or terms and conditions are not agreed.'); - this.nzNotification.error($localize`No NFTs to purchase or terms and conditions are not agreed.`, ''); + this.nzNotification.error( + $localize`No NFTs to purchase or terms and conditions are not agreed.`, + '', + ); return; } const bulkPurchaseRequest = { - orders: nfts + orders: nfts, }; // console.log('[checkout-overlay.component-proceddWithBulkOrder] params being passed for signing: ', bulkPurchaseRequest); await this.auth.sign(bulkPurchaseRequest, async (signedRequest, finish) => { this.notification - .processRequest(this.orderApi.orderNfts(signedRequest), $localize`Bulk order created.`, finish) + .processRequest( + this.orderApi.orderNfts(signedRequest), + $localize`Bulk order created.`, + finish, + ) .subscribe((transaction: Transaction | undefined) => { if (transaction) { this.transSubscription?.unsubscribe(); setItem(StorageItem.CheckoutTransaction, transaction.uid); - this.transSubscription = this.orderApi.listen(transaction.uid).subscribe(this.transaction$); - this.pushToHistory(transaction, transaction.uid, dayjs(), $localize`Waiting for transaction...`); + this.transSubscription = this.orderApi + .listen(transaction.uid) + .subscribe(this.transaction$); + this.pushToHistory( + transaction, + transaction.uid, + dayjs(), + $localize`Waiting for transaction...`, + ); } else { console.error('Transaction failed or did not return a valid transaction.'); - this.nzNotification.error($localize`Transaction failed or did not return a valid transaction.`, ''); + this.nzNotification.error( + $localize`Transaction failed or did not return a valid transaction.`, + '', + ); } }); }); diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts index ff52cf1..01abd0a 100644 --- a/src/app/components/cart/services/cart.service.ts +++ b/src/app/components/cart/services/cart.service.ts @@ -1,10 +1,6 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; -import { - Nft, - Collection, - MIN_AMOUNT_TO_TRANSFER, -} from '@build-5/interfaces'; +import { Nft, Collection, MIN_AMOUNT_TO_TRANSFER } from '@build-5/interfaces'; import { getItem, setItem, StorageItem } from './../../../@core/utils/local-storage.utils'; import { NzNotificationService } from 'ng-zorro-antd/notification'; import { HelperService } from '@pages/nft/services/helper.service'; @@ -18,10 +14,9 @@ export interface CartItem { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CartService { - private showCartSubject = new BehaviorSubject(false); public showCart$ = this.showCartSubject.asObservable(); private cartItemsSubject = new BehaviorSubject(this.loadCartItems()); @@ -50,13 +45,20 @@ export class CartService { console.log('[CartService] addToCart function called. Adding cart item: ', cartItem); const currentItems = this.cartItemsSubject.value; - const isItemAlreadyInCart = currentItems.some(item => item.nft.uid === cartItem.nft.uid); + const isItemAlreadyInCart = currentItems.some((item) => item.nft.uid === cartItem.nft.uid); if (!isItemAlreadyInCart) { const updatedCartItems = [...currentItems, cartItem]; this.cartItemsSubject.next(updatedCartItems); this.saveCartItems(); - this.notification.success($localize`NFT ` + cartItem.nft.name + ` from collection ` + cartItem.collection.name + ` has been added to your cart.`, ''); + this.notification.success( + $localize`NFT ` + + cartItem.nft.name + + ` from collection ` + + cartItem.collection.name + + ` has been added to your cart.`, + '', + ); // console.log('[CartService] NFT added to cart:', cartItem); } else { // console.log('[CartService] NFT is already in the cart:', cartItem); @@ -66,21 +68,26 @@ export class CartService { public removeFromCart(itemId: string): void { // console.log('[CartService] removeFromCart function called.'); - const updatedCartItems = this.cartItemsSubject.value.filter(item => item.nft.uid !== itemId); + const updatedCartItems = this.cartItemsSubject.value.filter((item) => item.nft.uid !== itemId); this.cartItemsSubject.next(updatedCartItems); this.saveCartItems(); // console.log('[CartService-removeFromCart] Cart updated:', updatedCartItems); } public removeItemsFromCart(itemIds: string[]): void { - const updatedCartItems = this.cartItemsSubject.value.filter(item => !itemIds.includes(item.nft.uid)); + const updatedCartItems = this.cartItemsSubject.value.filter( + (item) => !itemIds.includes(item.nft.uid), + ); this.cartItemsSubject.next(updatedCartItems); this.saveCartItems(); } public removeGroupItemsFromCart(tokenSymbol: string): void { - const updatedCartItems = this.cartItemsSubject.value.filter(item => { - const itemTokenSymbol = (item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network) || 'Unknown'; + const updatedCartItems = this.cartItemsSubject.value.filter((item) => { + const itemTokenSymbol = + (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) || 'Unknown'; return itemTokenSymbol !== tokenSymbol; }); this.cartItemsSubject.next(updatedCartItems); @@ -124,7 +131,10 @@ export class CartService { } public getAvailableNftQuantity(cartItem: CartItem): number { - const isAvailableForSale = this.helperService.isAvailableForSale(cartItem.nft, cartItem.collection); + const isAvailableForSale = this.helperService.isAvailableForSale( + cartItem.nft, + cartItem.collection, + ); // console.log("[cart.service-getAvailableNftQuantity] function called for cartItem.nft.name: " + cartItem.nft.name + ", isAvailableForSale: " + isAvailableForSale); if (cartItem.nft.placeholderNft && isAvailableForSale) { diff --git a/src/app/components/nft/components/nft-card/nft-card.component.html b/src/app/components/nft/components/nft-card/nft-card.component.html index 2e61b60..1120b0f 100644 --- a/src/app/components/nft/components/nft-card/nft-card.component.html +++ b/src/app/components/nft/components/nft-card/nft-card.component.html @@ -238,11 +238,11 @@ > - - { this.notification .processRequest(this.orderApi.orderNft(sc), $localize`Order created.`, finish) .subscribe((val: any) => { - console.log('[nft-this.checkoutService.component-proceedWithOrder] val after processing: ', val); + console.log( + '[nft-this.checkoutService.component-proceedWithOrder] val after processing: ', + val, + ); this.transSubscription?.unsubscribe(); setItem(StorageItem.CheckoutTransaction, val.uid); this.transSubscription = this.orderApi.listen(val.uid).subscribe(this.transaction$); diff --git a/src/app/pages/award/pages/new/new.page.ts b/src/app/pages/award/pages/new/new.page.ts index f0ef978..700132b 100644 --- a/src/app/pages/award/pages/new/new.page.ts +++ b/src/app/pages/award/pages/new/new.page.ts @@ -17,7 +17,9 @@ import { TEST_AVAILABLE_MINTABLE_NETWORKS, Token, TokenStatus, - getDefDecimalIfNotSet, Network } from '@build-5/interfaces'; + getDefDecimalIfNotSet, + Network, +} from '@build-5/interfaces'; import { BehaviorSubject, of, Subscription, switchMap } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { AwardApi } from './../../../../@api/award.api'; diff --git a/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts b/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts index 6292e0c..c345567 100644 --- a/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts +++ b/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts @@ -4,7 +4,7 @@ import { Nft, Collection } from '@build-5/interfaces'; import { CartService } from '@components/cart/services/cart.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CollectionNftStateService { private listedNftsSubject = new BehaviorSubject([]); @@ -12,9 +12,7 @@ export class CollectionNftStateService { private availableNftsCountSubject = new BehaviorSubject(0); public availableNftsCount$ = this.availableNftsCountSubject.asObservable(); - constructor( - private cartService: CartService, - ) { } + constructor(private cartService: CartService) {} public setListedNfts(nfts: Nft[], collection: Collection) { this.listedNftsSubject.next(nfts); @@ -22,7 +20,9 @@ export class CollectionNftStateService { } private updateAvailableNftsCount(nfts: Nft[], collection: Collection) { - const availableNftsCount = nfts.filter(nft => this.cartService.isNftAvailableForSale(nft, collection)).length; + const availableNftsCount = nfts.filter((nft) => + this.cartService.isNftAvailableForSale(nft, collection), + ).length; this.availableNftsCountSubject.next(availableNftsCount); } @@ -30,5 +30,3 @@ export class CollectionNftStateService { return this.listedNftsSubject.getValue(); } } - - diff --git a/src/app/pages/collection/pages/collection/nfts/nfts.page.html b/src/app/pages/collection/pages/collection/nfts/nfts.page.html index 407b310..f49c797 100644 --- a/src/app/pages/collection/pages/collection/nfts/nfts.page.html +++ b/src/app/pages/collection/pages/collection/nfts/nfts.page.html @@ -10,15 +10,26 @@
- + {{ state.nbHits | number }} records
- +
{{ sweepCount }} - +
@@ -38,7 +49,7 @@ >
Filters&Sort
-

+
{ @@ -91,16 +92,16 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { this.collectionNftStateService.setListedNfts(this.originalNfts, this.collection); } }, - error: err => { + error: (err) => { // console.error('Error fetching collection:', err); this.notification.error($localize`Error occurred while fetching collection.`, ''); - } + }, }); } this.collectionNftStateService.availableNftsCount$ .pipe(takeUntil(this.destroy$)) - .subscribe(count => { + .subscribe((count) => { this.availableNftsCount = count; }); @@ -166,40 +167,42 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { return; } - this.collectionApi.getCollectionById(this.collectionId) + this.collectionApi + .getCollectionById(this.collectionId) .pipe( take(1), filter((collection): collection is Collection => collection !== undefined), switchMap((collection: Collection) => { const listedNfts = this.collectionNftStateService.getListedNfts(); - const nftsForSale = listedNfts.filter(nft => - this.cartService.isNftAvailableForSale(nft, collection) + const nftsForSale = listedNfts.filter((nft) => + this.cartService.isNftAvailableForSale(nft, collection), ); - const nftsToAdd = nftsForSale - .slice(0, Math.min(count, 20)) - .sort((a, b) => { - const priceA = a.availablePrice != null ? a.availablePrice : 0; - const priceB = b.availablePrice != null ? b.availablePrice : 0; - return priceA - priceB; - }); + const nftsToAdd = nftsForSale.slice(0, Math.min(count, 20)).sort((a, b) => { + const priceA = a.availablePrice != null ? a.availablePrice : 0; + const priceB = b.availablePrice != null ? b.availablePrice : 0; + return priceA - priceB; + }); - nftsToAdd.forEach(nft => { + nftsToAdd.forEach((nft) => { const cartItem = { nft, collection, quantity: 1, salePrice: 0 }; this.cartService.addToCart(cartItem); }); - this.notification.success($localize`NFTs swept into your cart, open cart to review added items.`, ''); + this.notification.success( + $localize`NFTs swept into your cart, open cart to review added items.`, + '', + ); return nftsToAdd; }), - takeUntil(this.destroy$) + takeUntil(this.destroy$), ) .subscribe({ - error: err => { + error: (err) => { this.notification.error($localize`Error occurred while fetching collection.`, ''); - } + }, }); } diff --git a/src/app/pages/nft/pages/nft/nft.page.html b/src/app/pages/nft/pages/nft/nft.page.html index f77166b..fed7c6f 100644 --- a/src/app/pages/nft/pages/nft/nft.page.html +++ b/src/app/pages/nft/pages/nft/nft.page.html @@ -4,19 +4,19 @@ + > NFT + > {{ getCollectionTypeString(collectionType) }} + > {{ getCollectionStatusString(collectionMinting) }} {{ getTitle(data.nft$ | async) }}

nzBlock nzSize="large" i18n - style="margin-top:5px;margin-bottom:5px;" + style="margin-top: 5px; margin-bottom: 5px" > Add to Cart diff --git a/src/app/pages/nft/pages/nft/nft.page.ts b/src/app/pages/nft/pages/nft/nft.page.ts index e6c3926..ebab3fe 100644 --- a/src/app/pages/nft/pages/nft/nft.page.ts +++ b/src/app/pages/nft/pages/nft/nft.page.ts @@ -120,7 +120,7 @@ export class NFTPage implements OnInit, OnDestroy { } public ngOnInit(): void { - this.data.nft$.subscribe(nft => { + this.data.nft$.subscribe((nft) => { // console.log('[OnInit] Current NFT:', nft); this.currentNft = nft; }); @@ -418,7 +418,10 @@ export class NFTPage implements OnInit, OnDestroy { if (status === null || status === undefined) { return 'Unknown'; } - console.log('[nft.page-getCollectionStatusString] return collection status: ', CollectionStatus[status]); + console.log( + '[nft.page-getCollectionStatusString] return collection status: ', + CollectionStatus[status], + ); return CollectionStatus[status]; } @@ -555,7 +558,7 @@ export class NFTPage implements OnInit, OnDestroy { if (nft && this.data.collection$) { // console.log('[addToCart-this.data.collection$.value', this.data.collection$.value) // console.log('[addToCart-this.data.nft$.value', this.data.nft$.value) - this.data.collection$.pipe(take(1)).subscribe(collection => { + this.data.collection$.pipe(take(1)).subscribe((collection) => { if (collection) { this.cartService.addToCart({ nft, collection, quantity: 1, salePrice: 0 }); // console.log('Added to cart:', nft, collection); diff --git a/src/app/pages/nft/services/helper.service.ts b/src/app/pages/nft/services/helper.service.ts index 3a6b75f..f0e5081 100644 --- a/src/app/pages/nft/services/helper.service.ts +++ b/src/app/pages/nft/services/helper.service.ts @@ -127,12 +127,10 @@ export class HelperService { // console.log(`[getDate] Original input:`, date); if (typeof date === 'object') { if (date?.toDate) { - const dateFromObject = date.toDate(); // console.log(`[getDate] Object with toDate method detected, toDate result:`, dateFromObject); return dateFromObject; } else if (date?.seconds) { - const dateFromSeconds = new Date(date.seconds * 1000); // Convert to milliseconds // console.log(`[getDate] Object with seconds property detected, converted to Date:`, dateFromSeconds); return dateFromSeconds; @@ -180,12 +178,10 @@ export class HelperService { return false; } - - const isAvail = ( + const isAvail = col.approved === true && !!this.getDate(nft.availableFrom) && - dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's') - ); + dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's'); // console.log("[NFT helper.service.ts] isAvailableForSale function returning " + isAvail + ". nft name: " + nft?.name + ", col name: " + col?.name + ". nft.availableFrom: " + nft.availableFrom.seconds); // console.log("col.approved: " + (col.approved === true)); // console.log("!!this.getDate(nft.availableFrom): " + !!this.getDate(nft.availableFrom) + ", this.getDate(nft.availableFrom): " + this.getDate(nft.availableFrom)); From 9b50b4961995681366581c8e280ccc196621e15c Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Thu, 25 Jan 2024 22:27:26 -0500 Subject: [PATCH 05/24] 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; From cc0aa93b89d5c5558d2274a25a02ed8008bdb868 Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Tue, 30 Jan 2024 00:26:53 -0500 Subject: [PATCH 06/24] Improved cart quantities and fixed Sweep to Cart function --- .../filter-storage/filter-storage.service.ts | 2 +- .../cart-modal/cart-modal.component.html | 16 +- .../cart-modal/cart-modal.component.ts | 42 +++-- .../checkout/checkout-overlay.component.html | 12 +- .../components/cart/services/cart.service.ts | 30 ++-- .../collection-card.component.html | 6 +- .../member-edit-drawer.component.html | 4 +- .../nft-checkout/nft-checkout.component.ts | 3 + .../pages/collection/collection.page.ts | 13 +- .../collection/nfts/collectionNfts.service.ts | 5 + .../pages/collection/nfts/nfts.page.html | 37 +++-- .../pages/collection/nfts/nfts.page.ts | 150 +++++++++++++----- src/app/pages/nft/pages/nft/nft.page.html | 85 +++++----- src/app/pages/nft/pages/nft/nft.page.ts | 25 +-- src/app/pages/nft/services/helper.service.ts | 65 ++++++-- 15 files changed, 301 insertions(+), 194 deletions(-) diff --git a/src/app/@core/services/filter-storage/filter-storage.service.ts b/src/app/@core/services/filter-storage/filter-storage.service.ts index b9a092c..f1020c7 100644 --- a/src/app/@core/services/filter-storage/filter-storage.service.ts +++ b/src/app/@core/services/filter-storage/filter-storage.service.ts @@ -195,7 +195,7 @@ export class FilterStorageService { public marketNftsResetVisible$: BehaviorSubject = new BehaviorSubject(false); public marketNftsFilters$: BehaviorSubject = new BehaviorSubject({ - sortBy: this.marketNftsFiltersOptions.sortItems[0].value, + sortBy: this.marketNftsFiltersOptions.sortItems[2].value, }); public marketCollectionsFiltersOptions = { diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.html b/src/app/components/cart/components/cart-modal/cart-modal.component.html index 63213ac..ad1d1d0 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.html +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.html @@ -100,11 +100,11 @@ *ngIf="discount(item.collection, item.nft) < 1" > {{ - cartItemPrices[item.nft.uid]?.originalPrice + cartItemPrices[item.nft.uid].originalPrice | formatToken - : (item.nft?.placeholderNft - ? item.collection?.mintingData?.network - : item.nft?.mintingData?.network) + : (item.nft.placeholderNft + ? item.collection.mintingData?.network + : item.nft.mintingData?.network) : true : true | async @@ -112,11 +112,11 @@
{{ - cartItemPrices[item.nft.uid]?.discountedPrice + cartItemPrices[item.nft.uid].discountedPrice | formatToken - : (item.nft?.placeholderNft - ? item.collection?.mintingData?.network - : item.nft?.mintingData?.network) + : (item.nft.placeholderNft + ? item.collection.mintingData?.network + : item.nft.mintingData?.network) : true : true | async diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.ts b/src/app/components/cart/components/cart-modal/cart-modal.component.ts index cb354c4..ae7d3e5 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.ts +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.ts @@ -46,7 +46,6 @@ export class CartModalComponent implements OnInit, OnDestroy { ngOnInit() { this.subscriptions.add( this.cartService.getCartItems().subscribe((items) => { - // console.log('[CartModalComponent-ngOnInit] Cart items updated:', items); this.cartItemsStatus = items.map((item) => this.cartItemStatus(item)); this.cartItemsQuantities = items.map((item) => this.cartItemSaleAvailableQty(item)); items.forEach((item) => { @@ -77,19 +76,15 @@ export class CartModalComponent implements OnInit, OnDestroy { } private refreshCartData() { - // console.log('Refreshing cart items...'); const cartItems = this.cartService.getCartItems().getValue(); - // console.log('Current cart items:', cartItems); const freshDataObservables = cartItems.map((item) => this.nftApi.getNftById(item.nft.uid).pipe( take(1), map((freshNft) => { - // console.log(`Fetched fresh data for NFT ${item.nft.uid}:`, freshNft); return freshNft ? { ...item, nft: freshNft } : item; }), catchError((error) => { - // console.error(`Error fetching fresh data for NFT ${item.nft.uid}:`, error); return of(item); }), ), @@ -97,8 +92,6 @@ export class CartModalComponent implements OnInit, OnDestroy { forkJoin(freshDataObservables).subscribe( (freshCartItems) => { - // console.log('Fresh cart items:', freshCartItems); - this.cartService.updateCartItems(freshCartItems); this.cartItemsStatus = freshCartItems.map((item) => this.cartItemStatus(item)); @@ -111,8 +104,6 @@ export class CartModalComponent implements OnInit, OnDestroy { this.cartItemPrices[item.nft.uid] = { originalPrice, discountedPrice }; }); - // console.log('Finished refreshing cart items.'); - this.cd.markForCheck(); }, (error) => { @@ -124,17 +115,25 @@ export class CartModalComponent implements OnInit, OnDestroy { public updateQuantity(event: Event, itemId: string): void { const inputElement = event.target as HTMLInputElement; - const newQuantity = Number(inputElement.value); - - if (newQuantity === 0) { - this.cartService.removeFromCart(itemId); - } else { - const cartItems = this.cartService.getCartItems().getValue(); - const itemIndex = cartItems.findIndex((cartItem) => cartItem.nft.uid === itemId); - if (itemIndex !== -1) { - cartItems[itemIndex].quantity = newQuantity; - this.cartService.saveCartItems(); + let newQuantity = Number(inputElement.value); + + const cartItems = this.cartService.getCartItems().getValue(); + const itemIndex = cartItems.findIndex((cartItem) => cartItem.nft.uid === itemId); + + if (itemIndex !== -1) { + const maxQuantity = this.cartItemsQuantities[itemIndex]; + const minQuantity = 1; + + if (newQuantity < minQuantity) { + newQuantity = minQuantity; + inputElement.value = minQuantity.toString(); + } else if (newQuantity > maxQuantity) { + newQuantity = maxQuantity; + inputElement.value = maxQuantity.toString(); } + + cartItems[itemIndex].quantity = newQuantity; + this.cartService.saveCartItems(); } } @@ -170,20 +169,15 @@ export class CartModalComponent implements OnInit, OnDestroy { } public cartItemStatus(item: CartItem): any { - // console.log("[cart-modal.component-cartItemStatus] function called"); const itemAvailable = this.cartService.isCartItemAvailableForSale(item); if (itemAvailable) { - // console.log("[cart-modal.component-cartItemStatus] returning Available, itemAvailable: " + itemAvailable); return 'Available'; } - // console.log("[cart-modal.component-cartItemStatus] returning Not Available, itemAvailable: " + itemAvailable); return 'Not Available'; } private cartItemSaleAvailableQty(item: CartItem): number { - // console.log("[cart-modal.component-cartItemSaleAvailableQty] function called"); const availQty = this.cartService.getAvailableNftQuantity(item); - // console.log("[cart-modal.component] cartItemSaleAvailableQty, qty: " + availQty); return availQty; } diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.html b/src/app/components/cart/components/checkout/checkout-overlay.component.html index a1be5fd..140a55a 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.html +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.html @@ -58,9 +58,9 @@

Network/Token: {{ group.tokenSy {{ item.salePrice | formatToken - : (item.nft?.placeholderNft - ? item.collection?.mintingData?.network - : item.nft?.mintingData?.network) + : (item.nft.placeholderNft + ? item.collection.mintingData?.network + : item.nft.mintingData?.network) : true : true | async @@ -70,9 +70,9 @@

Network/Token: {{ group.tokenSy {{ item.quantity * item.salePrice | formatToken - : (item.nft?.placeholderNft - ? item.collection?.mintingData?.network - : item.nft?.mintingData?.network) + : (item.nft.placeholderNft + ? item.collection.mintingData?.network + : item.nft.mintingData?.network) : true : true | async diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts index 01abd0a..b4cc607 100644 --- a/src/app/components/cart/services/cart.service.ts +++ b/src/app/components/cart/services/cart.service.ts @@ -25,9 +25,7 @@ export class CartService { private notification: NzNotificationService, private helperService: HelperService, public auth: AuthService, - ) { - // console.log('CartService instance created'); - } + ) {} public showCart(): void { this.showCartSubject.next(true); @@ -59,9 +57,7 @@ export class CartService { ` has been added to your cart.`, '', ); - // console.log('[CartService] NFT added to cart:', cartItem); } else { - // console.log('[CartService] NFT is already in the cart:', cartItem); this.notification.error($localize`This NFT already exists in your cart.`, ''); } } @@ -71,7 +67,6 @@ export class CartService { const updatedCartItems = this.cartItemsSubject.value.filter((item) => item.nft.uid !== itemId); this.cartItemsSubject.next(updatedCartItems); this.saveCartItems(); - // console.log('[CartService-removeFromCart] Cart updated:', updatedCartItems); } public removeItemsFromCart(itemIds: string[]): void { @@ -95,33 +90,32 @@ export class CartService { } public getCartItems(): BehaviorSubject { - // console.log('[CartService] getCartItems function called.'); return this.cartItemsSubject; } public updateCartItems(updatedItems: CartItem[]): void { - this.cartItemsSubject.next(updatedItems); // Update the BehaviorSubject with the new cart items - this.saveCartItems(); // Save the updated cart items to local storage or your backend + this.cartItemsSubject.next(updatedItems); + this.saveCartItems(); } public saveCartItems(): void { - // console.log('[CartService] getCartItems function called.'); setItem(StorageItem.CartItems, this.cartItemsSubject.value); - // console.log('[CartService] Saving cart items to local storage:', this.cartItemsSubject.value); } private loadCartItems(): CartItem[] { - // console.log('[CartService] Loading cart items from local storage'); const items = getItem(StorageItem.CartItems) as CartItem[]; - // console.log('[CartService] Cart items loaded from local storage:', items); return items || []; } public isNftAvailableForSale(nft: Nft, collection: Collection): boolean { const isLocked = this.helperService.isLocked(nft, collection, true); - const isOwner = nft.owner === this.auth.member$.value?.uid; + + let isOwner = false; + if (nft.owner != null && this.auth.member$.value?.uid != null) { + isOwner = nft.owner === this.auth.member$.value?.uid; + } + const availableForSale = this.helperService.isAvailableForSale(nft, collection); - // console.log(`[cart.service-isNftAvailableForSale] results for NFT ${nft.name}; availableForSale: ${availableForSale}, isLocked: ${isLocked}, isOwner: ${isOwner}`); return !isLocked && availableForSale && (!isOwner || !nft.owner); } @@ -135,16 +129,12 @@ export class CartService { cartItem.nft, cartItem.collection, ); - // console.log("[cart.service-getAvailableNftQuantity] function called for cartItem.nft.name: " + cartItem.nft.name + ", isAvailableForSale: " + isAvailableForSale); if (cartItem.nft.placeholderNft && isAvailableForSale) { - // console.log("[service-getAvailableNftQuantity] returning qty, placeholder: " + cartItem.nft.placeholderNft + ". availableNfts " + cartItem.collection.availableNfts); return cartItem.collection.availableNfts || 0; } else if (isAvailableForSale) { - // console.log("[service-getAvailableNftQuantity] returning 1, isAvailableForSale: " + isAvailableForSale + ". placeholder: " + cartItem.nft.placeholderNft); return 1; } - // console.log("[service-getAvailableNftQuantity] returning 0, isAvailableForSale: " + isAvailableForSale + ". placeholder: " + cartItem.nft.placeholderNft); return 0; } @@ -176,6 +166,6 @@ export class CartService { public calcPrice(item: CartItem, discount: number): number { const itemPrice = item.nft?.availablePrice || item.nft?.price || 0; - return this.calc(itemPrice, discount); // assuming calc method applies the discount + return this.calc(itemPrice, discount); } } diff --git a/src/app/components/collection/components/collection-card/collection-card.component.html b/src/app/components/collection/components/collection-card/collection-card.component.html index 088f29a..802d069 100644 --- a/src/app/components/collection/components/collection-card/collection-card.component.html +++ b/src/app/components/collection/components/collection-card/collection-card.component.html @@ -106,9 +106,9 @@ class="text-sm font-medium truncate text-foregrounds-primary dark:text-foregrounds-primary-dark" > {{ - collection?.floorPrice - ? (collection?.floorPrice - | formatToken : collection?.mintingData?.network : false : true : 2 + collection.floorPrice + ? (collection.floorPrice + | formatToken : collection.mintingData?.network : false : true : 2 | async) : '-' }} diff --git a/src/app/components/member/components/member-edit-drawer/member-edit-drawer.component.html b/src/app/components/member/components/member-edit-drawer/member-edit-drawer.component.html index 7048756..31bf548 100644 --- a/src/app/components/member/components/member-edit-drawer/member-edit-drawer.component.html +++ b/src/app/components/member/components/member-edit-drawer/member-edit-drawer.component.html @@ -84,7 +84,7 @@

Avatar

[nzValue]="s.value" > Avatar

(); public purchasedNft?: Nft | null; @@ -156,6 +158,7 @@ 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) => { diff --git a/src/app/pages/collection/pages/collection/collection.page.ts b/src/app/pages/collection/pages/collection/collection.page.ts index 2094b3c..1ef8688 100644 --- a/src/app/pages/collection/pages/collection/collection.page.ts +++ b/src/app/pages/collection/pages/collection/collection.page.ts @@ -32,7 +32,15 @@ import { RANKING_TEST, } from '@build-5/interfaces'; import { NzNotificationService } from 'ng-zorro-antd/notification'; -import { BehaviorSubject, first, firstValueFrom, skip, Subscription } from 'rxjs'; +import { + Subject, + BehaviorSubject, + first, + firstValueFrom, + skip, + Subscription, + takeUntil, +} from 'rxjs'; import { DataService } from '../../services/data.service'; import { NotificationService } from './../../../../@core/services/notification/notification.service'; @@ -51,6 +59,7 @@ export class CollectionPage implements OnInit, OnDestroy { private guardiansSubscription$?: Subscription; private guardiansRankModeratorSubscription$?: Subscription; private subscriptions$: Subscription[] = []; + private destroy$ = new Subject(); constructor( public deviceService: DeviceService, @@ -309,6 +318,8 @@ export class CollectionPage implements OnInit, OnDestroy { public ngOnDestroy(): void { this.cancelSubscriptions(); this.guardiansSubscription$?.unsubscribe(); + this.destroy$.next(); + this.destroy$.complete(); } public get networkTypes(): typeof Network { diff --git a/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts b/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts index c345567..59010db 100644 --- a/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts +++ b/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts @@ -20,6 +20,11 @@ export class CollectionNftStateService { } private updateAvailableNftsCount(nfts: Nft[], collection: Collection) { + console.log( + '[collectionNfts.service-updateAvailableNftsCount] function called with (nfts, collection): ', + nfts, + collection, + ); const availableNftsCount = nfts.filter((nft) => this.cartService.isNftAvailableForSale(nft, collection), ).length; diff --git a/src/app/pages/collection/pages/collection/nfts/nfts.page.html b/src/app/pages/collection/pages/collection/nfts/nfts.page.html index f49c797..0e647f4 100644 --- a/src/app/pages/collection/pages/collection/nfts/nfts.page.html +++ b/src/app/pages/collection/pages/collection/nfts/nfts.page.html @@ -11,23 +11,34 @@
{{ state.nbHits | number }} records -
- - +
+ + + {{ sweepCount }} + + + No NFTs available to sweep. +
- {{ sweepCount }} -
diff --git a/src/app/pages/collection/pages/collection/nfts/nfts.page.ts b/src/app/pages/collection/pages/collection/nfts/nfts.page.ts index 42e3477..db9b7a6 100644 --- a/src/app/pages/collection/pages/collection/nfts/nfts.page.ts +++ b/src/app/pages/collection/pages/collection/nfts/nfts.page.ts @@ -6,6 +6,7 @@ import { OnChanges, OnInit, OnDestroy, + SimpleChanges, } from '@angular/core'; import { NftApi } from '@api/nft.api'; import { CollectionApi } from '@api/collection.api'; @@ -26,7 +27,6 @@ import { switchMap } from 'rxjs/operators'; import { CartService } from '@components/cart/services/cart.service'; import { NzNotificationService } from 'ng-zorro-antd/notification'; import { state } from '@angular/animations'; - import { CollectionNftStateService } from './collectionNfts.service'; // used in src/app/pages/collection/pages/collection/collection.page.ts @@ -81,61 +81,91 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { ) {} public ngOnInit(): void { - if (this.collectionId) { - this.collectionApi - .getCollectionById(this.collectionId) - .pipe(take(1)) - .subscribe({ - next: (collectionData) => { - if (collectionData) { - this.collection = collectionData; - this.collectionNftStateService.setListedNfts(this.originalNfts, this.collection); - } - }, - error: (err) => { - // console.error('Error fetching collection:', err); - this.notification.error($localize`Error occurred while fetching collection.`, ''); - }, - }); - } - + console.log('ngOnInit fires'); this.collectionNftStateService.availableNftsCount$ .pipe(takeUntil(this.destroy$)) .subscribe((count) => { this.availableNftsCount = count; + console.log('[ngOnInit] this.availableNftsCount is set to count: ', count); + this.cd.markForCheck(); }); - // Algolia change detection bug fix - setInterval(() => this.cd.markForCheck(), 500); + if (this.collectionId) { + this.loadCollection(this.collectionId); + } } - public ngOnChanges(): void { - // TODO comeup with better process. - setTimeout(() => { + public ngOnChanges(changes: SimpleChanges): void { + console.log('ngOnChanges fires'); + if (changes.collectionId) { + this.resetComponentState(); if (this.collectionId) { - this.filterStorageService.marketNftsFilters$.next({ - ...this.filterStorageService.marketNftsFilters$.value, - refinementList: { - ...this.filterStorageService.marketNftsFilters$.value.refinementList, - collection: [this.collectionId], - }, - }); - - this.config = { - indexName: COL.NFT, - searchClient: this.algoliaService.searchClient, - initialUiState: { - nft: this.filterStorageService.marketNftsFilters$.value, - }, - }; + this.loadCollection(this.collectionId); } - }, 500); + } + } + + private resetComponentState(): void { + console.log('resetComponentState fires'); + this.availableNftsCount = 0; + this.sweepCount = 1; + this.cd.markForCheck(); + } + + private loadCollection(collectionId: string): void { + console.log(`loadCollection fires for collectionId: ${collectionId}`); + this.collectionApi + .getCollectionById(collectionId) + .pipe(take(1)) + .subscribe({ + next: (collectionData) => { + console.log('Full response from getCollectionById:', collectionData); + if (collectionData) { + this.collection = collectionData; + const listedNfts = this.collectionNftStateService.getListedNfts(); + if (this.originalNfts.length > 0 && listedNfts.length === 0) { + this.collectionNftStateService.setListedNfts(this.originalNfts, this.collection); + } + this.initializeAlgoliaFilters(collectionId); + } + }, + error: (err) => { + this.notification.error($localize`Error occurred while fetching collection.`, ''); + }, + }); + } + + private initializeAlgoliaFilters(collectionId: string): void { + console.log('initializeAlgoliaFilters fires with collectionId: ', collectionId); + + console.log('Current filters:', this.filterStorageService.marketNftsFilters$.value); + this.filterStorageService.marketNftsFilters$.next({ + ...this.filterStorageService.marketNftsFilters$.value, + refinementList: { + ...this.filterStorageService.marketNftsFilters$.value.refinementList, + collection: [collectionId], + }, + }); + console.log('Updated filters:', this.filterStorageService.marketNftsFilters$.value); + + this.config = { + indexName: COL.NFT, + searchClient: this.algoliaService.searchClient, + initialUiState: { + nft: this.filterStorageService.marketNftsFilters$.value, + }, + }; + + this.cd.markForCheck(); } public captureOriginalHits(hits: any[]) { + console.log('captureOriginalHits fires'); + // console.log('[nfts.page-captureOriginalHits] function called with following hits (hits, this.collection): ', hits, this.collection); if (hits && hits.length > 0 && this.collection) { this.originalNfts = hits; this.collectionNftStateService.setListedNfts(hits, this.collection); + // console.log('[nfts.page-captureOriginalHits] hits if passed, originalNfts set to hits and collectionNftStateService.setListedNfts given (hits, this.collection): ', hits, this.collection); } } @@ -144,12 +174,22 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { } public convertAllToSoonaverseModel = (algoliaItems: any[]) => { - this.captureOriginalHits(algoliaItems); + console.log('convertAllToSoonaverseModel fires'); + // console.log('[nfts.page-convertAllToSoonaverseModel] function called with following algoliaItems: ', algoliaItems); + if (this.originalNfts.length !== algoliaItems.length && algoliaItems.length > 0) { + // console.log('[nfts.page-convertAllToSoonaverseModel] run captureOriginalHits since aligoliaItems and originalNfts lengths dont match, (alogliaItems.length, originalNfts.length): ', algoliaItems.length, this.originalNfts.length); + this.captureOriginalHits(algoliaItems); + } + console.log('Before processing:', algoliaItems); const transformedItems = algoliaItems.map((algolia) => ({ ...algolia, availableFrom: Timestamp.fromMillis(+algolia.availableFrom), })); + console.log('After processing:', transformedItems); + + this.cd.markForCheck(); + return transformedItems; }; @@ -162,6 +202,7 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { } public sweepToCart(count: number) { + console.log('[nfts.page-sweepToCart] function called with following count: ', count); if (!this.collectionId) { this.notification.error($localize`Collection ID is not available.`, ''); return; @@ -174,16 +215,36 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { filter((collection): collection is Collection => collection !== undefined), switchMap((collection: Collection) => { const listedNfts = this.collectionNftStateService.getListedNfts(); + console.log( + '[nfts.page-sweepToCart] listedNfts set to this.collectionNftStateService: ', + listedNfts, + ); const nftsForSale = listedNfts.filter((nft) => this.cartService.isNftAvailableForSale(nft, collection), ); + console.log( + '[nfts.page-sweepToCart] nftsForSale set to filtered listedNfts that pass true for "isNftAvailableForSale", nftsForSale: ', + nftsForSale, + ); - const nftsToAdd = nftsForSale.slice(0, Math.min(count, 20)).sort((a, b) => { - const priceA = a.availablePrice != null ? a.availablePrice : 0; - const priceB = b.availablePrice != null ? b.availablePrice : 0; + const getEffectivePrice = (nft) => nft?.availablePrice || nft?.price || 0; + + const sortedNfts = nftsForSale.sort((a, b) => { + const priceA = getEffectivePrice(a); + const priceB = getEffectivePrice(b); return priceA - priceB; }); + console.log( + '[nfts.page-sweepToCart] sortedNfts set to nftsToAdd sorted by "effective price" low to high, sortedNfts: ', + sortedNfts, + ); + + const nftsToAdd = sortedNfts.slice(0, Math.min(count, sortedNfts.length)); + console.log( + '[nfts.page-sweepToCart] nftsToAdd set to first N sorted NFTs based on price, nftsToAdd: ', + nftsToAdd, + ); nftsToAdd.forEach((nft) => { const cartItem = { nft, collection, quantity: 1, salePrice: 0 }; @@ -207,6 +268,7 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { } public ngOnDestroy(): void { + console.log('ngOnDestroy fires'); this.destroy$.next(); this.destroy$.complete(); } diff --git a/src/app/pages/nft/pages/nft/nft.page.html b/src/app/pages/nft/pages/nft/nft.page.html index fed7c6f..247ddc2 100644 --- a/src/app/pages/nft/pages/nft/nft.page.html +++ b/src/app/pages/nft/pages/nft/nft.page.html @@ -13,12 +13,6 @@ > {{ getCollectionTypeString(collectionType) }} - - {{ getCollectionStatusString(collectionMinting) }} - {{ getTitle(data.nft$ | async) }}

> -
-
- - - - - - - - -
-
+ + + + + + + + + + +
@@ -805,6 +801,7 @@

[isOpen]="isCheckoutOpen" [nft]="data.nft$ | async" [collection]="data.collection$ | async" + [nftQuantity]="nftQtySelected" (wenOnClose)="isCheckoutOpen = false" > { - // console.log('[OnInit] Current NFT:', nft); this.currentNft = nft; }); this.deviceService.viewWithSearch$.next(false); @@ -279,8 +279,6 @@ export class NFTPage implements OnInit, OnDestroy { this.data.collection$.pipe(skip(1), untilDestroyed(this)).subscribe(async (p) => { if (p) { this.collectionType = p.type; - // this.collectionMinting = p.status; - // console.log('[nft.page-p] collectionMinting set to: ', p.status) this.collectionSubscriptions$.forEach((s) => { s.unsubscribe(); }); @@ -476,6 +474,7 @@ export class NFTPage implements OnInit, OnDestroy { } public buy(event?: MouseEvent): void { + console.log('Buy now NFT button pressed, qty to pass: ', this.nftQtySelected); if (event) { event.stopPropagation(); event.preventDefault(); @@ -556,18 +555,22 @@ export class NFTPage implements OnInit, OnDestroy { public addToCart(nft: Nft): void { if (nft && this.data.collection$) { - // console.log('[addToCart-this.data.collection$.value', this.data.collection$.value) - // console.log('[addToCart-this.data.nft$.value', this.data.nft$.value) this.data.collection$.pipe(take(1)).subscribe((collection) => { if (collection) { - this.cartService.addToCart({ nft, collection, quantity: 1, salePrice: 0 }); - // console.log('Added to cart:', nft, collection); - } else { - // console.error('Collection is undefined or null'); + this.cartService.addToCart({ + nft, + collection, + quantity: this.nftQtySelected, + salePrice: 0, + }); + console.log( + 'Added to cart (nft, collection, qty):', + nft, + collection, + this.nftQtySelected, + ); } }); - } else { - // console.error('NFT is undefined or null'); } } diff --git a/src/app/pages/nft/services/helper.service.ts b/src/app/pages/nft/services/helper.service.ts index f0e5081..13dc28c 100644 --- a/src/app/pages/nft/services/helper.service.ts +++ b/src/app/pages/nft/services/helper.service.ts @@ -111,6 +111,10 @@ export class HelperService { } public isDateInFuture(date?: Timestamp | null): boolean { + if (!date) { + return false; + } + if (!this.getDate(date)) { return false; } @@ -124,19 +128,40 @@ export class HelperService { } public getDate(date: any): any { - // console.log(`[getDate] Original input:`, date); - if (typeof date === 'object') { - if (date?.toDate) { - const dateFromObject = date.toDate(); - // console.log(`[getDate] Object with toDate method detected, toDate result:`, dateFromObject); - return dateFromObject; - } else if (date?.seconds) { - const dateFromSeconds = new Date(date.seconds * 1000); // Convert to milliseconds - // console.log(`[getDate] Object with seconds property detected, converted to Date:`, dateFromSeconds); - return dateFromSeconds; + if (!date) { + console.warn('getDate called with null or undefined:', date); + return undefined; + } + + if (typeof date === 'number') { + return new Date(date); + } else if (typeof date === 'object') { + // Checking if toDate exists and is a function + if (date.toDate && typeof date.toDate === 'function') { + try { + return date.toDate(); + } catch (e) { + console.error('Error calling toDate:', e); + } + } + + // Checking if seconds exists and is a number + if ('seconds' in date && !isNaN(Number(date.seconds))) { + const seconds = Number(date.seconds); + return new Date(seconds * 1000); + } + + // Checking if toMillis exists and is a function + if (date.toMillis && typeof date.toMillis === 'function') { + try { + return new Date(date.toMillis()); + } catch (e) { + console.error('Error calling toMillis:', e); + } } } - // console.log(`[getDate] Returning undefined, input could not be parsed as a date.`); + + console.warn('Unrecognized date format:', date); return undefined; } @@ -172,9 +197,7 @@ export class HelperService { } public isAvailableForSale(nft?: Nft | null, col?: Collection | null): boolean { - // console.log("[NFThelper-isAvailableForSale] function called"); if (!col || !nft?.availableFrom || col?.status === CollectionStatus.MINTING) { - // console.log("[NFT helper.service.ts] isAvailableForSale function returning false. nft name: " + nft?.name + ", col name: " + col?.name) return false; } @@ -182,13 +205,21 @@ export class HelperService { col.approved === true && !!this.getDate(nft.availableFrom) && dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's'); - // console.log("[NFT helper.service.ts] isAvailableForSale function returning " + isAvail + ". nft name: " + nft?.name + ", col name: " + col?.name + ". nft.availableFrom: " + nft.availableFrom.seconds); - // console.log("col.approved: " + (col.approved === true)); - // console.log("!!this.getDate(nft.availableFrom): " + !!this.getDate(nft.availableFrom) + ", this.getDate(nft.availableFrom): " + this.getDate(nft.availableFrom)); - // console.log("dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's')" + dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's')); return isAvail; } + 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; From 0ee051db0e4326aadeefd64f4e74d18aabe00aad Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Mon, 5 Feb 2024 19:24:57 -0500 Subject: [PATCH 07/24] Major revisions based on initial feedback --- .../filter-storage/filter-storage.service.ts | 4 +- src/app/@core/utils/local-storage.utils.ts | 18 + .../@shell/ui/header/header.component.html | 5 +- src/app/@shell/ui/header/header.component.ts | 165 +++--- src/app/components/cart/cart.module.ts | 8 +- .../cart-modal/cart-modal.component.html | 125 ++++- .../cart-modal/cart-modal.component.ts | 118 ++--- .../checkout/checkout-overlay.component.html | 242 +++++---- .../checkout/checkout-overlay.component.less | 17 + .../checkout/checkout-overlay.component.ts | 253 ++++++--- .../components/cart/services/cart.service.ts | 495 ++++++++++++++++-- .../nft-card/nft-card.component.html | 51 +- .../components/nft-card/nft-card.component.ts | 11 +- .../nft-checkout/nft-checkout.component.ts | 50 +- .../pages/collection/collection.page.ts | 10 +- .../collection/nfts/collectionNfts.service.ts | 23 +- .../pages/collection/nfts/nfts.page.ts | 41 +- src/app/pages/nft/pages/nft/nft.page.html | 46 +- src/app/pages/nft/pages/nft/nft.page.ts | 21 +- src/app/pages/nft/services/helper.service.ts | 19 +- src/theme/01-base/font.less | 4 + tailwind.config.js | 6 + 22 files changed, 1201 insertions(+), 531 deletions(-) diff --git a/src/app/@core/services/filter-storage/filter-storage.service.ts b/src/app/@core/services/filter-storage/filter-storage.service.ts index f1020c7..cda5d5d 100644 --- a/src/app/@core/services/filter-storage/filter-storage.service.ts +++ b/src/app/@core/services/filter-storage/filter-storage.service.ts @@ -195,7 +195,9 @@ export class FilterStorageService { public marketNftsResetVisible$: BehaviorSubject = new BehaviorSubject(false); public marketNftsFilters$: BehaviorSubject = new BehaviorSubject({ - sortBy: this.marketNftsFiltersOptions.sortItems[2].value, + sortBy: + this.marketNftsFiltersOptions.sortItems.find((item) => item.value === 'nft_price_asc') + ?.value || 'nft_availableFrom_asc', }); public marketCollectionsFiltersOptions = { diff --git a/src/app/@core/utils/local-storage.utils.ts b/src/app/@core/utils/local-storage.utils.ts index 7f2722e..b4d1c3e 100644 --- a/src/app/@core/utils/local-storage.utils.ts +++ b/src/app/@core/utils/local-storage.utils.ts @@ -28,6 +28,11 @@ export enum StorageItem { CartItems = 'App/cartItems', } +interface CheckoutTransactionData { + transactionId: string | null; + source: 'nftCheckout' | 'cartCheckout' | 'bulkNftCheckout' | null; +} + export const getBitItemItem = (nftId: string): unknown | null => { const item = localStorage.getItem(StorageItem.BidTransaction + nftId); return item ? JSON.parse(item) : null; @@ -135,3 +140,16 @@ export const setItem = (itemName: StorageItem, value: unknown): void => { export const removeItem = (itemName: StorageItem): void => { localStorage.removeItem(itemName); }; + +export const getCheckoutTransaction = (): CheckoutTransactionData | null => { + const item = localStorage.getItem(StorageItem.CheckoutTransaction); + return item ? JSON.parse(item) : { transactionId: null, source: null }; +}; + +export const setCheckoutTransaction = (value: CheckoutTransactionData): void => { + if (value.transactionId === null && value.source === null) { + localStorage.removeItem(StorageItem.CheckoutTransaction); + } else { + localStorage.setItem(StorageItem.CheckoutTransaction, JSON.stringify(value)); + } +}; diff --git a/src/app/@shell/ui/header/header.component.html b/src/app/@shell/ui/header/header.component.html index d6222f3..4a5bb19 100644 --- a/src/app/@shell/ui/header/header.component.html +++ b/src/app/@shell/ui/header/header.component.html @@ -48,7 +48,7 @@ nzType="default" nzShape="circle" class="relative inline-flex items-center justify-center border-0 wen-header-button ml-0 mr-2" - (click)="openShoppingCart()" + (click)="handleOpenCartModal()" > + = new BehaviorSubject([]); @@ -95,9 +91,9 @@ export class HeaderComponent implements OnInit, OnDestroy { private subscriptionTransaction$?: Subscription; private subscriptionNotification$?: Subscription; public cartItemCount = 0; - private cartItemsSubscription!: Subscription; - - @Output() openCartModal = new EventEmitter(); + private cartItemsSubscription$!: Subscription; + public isTransactionPending = false; + public isCartCheckoutOverlayVisible = false; constructor( public auth: AuthService, @@ -114,7 +110,6 @@ export class HeaderComponent implements OnInit, OnDestroy { private nzNotification: NzNotificationService, private checkoutService: CheckoutService, public cartService: CartService, - private modalService: NzModalService, ) {} public ngOnInit(): void { @@ -135,7 +130,7 @@ export class HeaderComponent implements OnInit, OnDestroy { } }); - this.cartItemsSubscription = this.cartService.getCartItems().subscribe((items) => { + this.cartItemsSubscription$ = this.cartService.getCartItems().subscribe((items) => { this.cartItemCount = items.length; }); @@ -179,6 +174,7 @@ export class HeaderComponent implements OnInit, OnDestroy { } if (expired === false && o?.payload.void === false && o.payload.reconciled === false) { + this.isTransactionPending = true; if (!this.notificationRef) { this.notificationRef = this.nzNotification.template(this.notCompletedNotification, { nzDuration: 0, @@ -186,6 +182,7 @@ export class HeaderComponent implements OnInit, OnDestroy { }); } } else { + this.isTransactionPending = false; this.removeCheckoutNotification(); } }); @@ -194,17 +191,24 @@ export class HeaderComponent implements OnInit, OnDestroy { interval(500) .pipe(untilDestroyed(this)) .subscribe(() => { - if (this.checkoutService.modalOpen$.value) { + if ( + this.checkoutService.modalOpen$.value || + this.cartService.checkoutOverlayOpenSubject$.value + ) { this.removeCheckoutNotification(false); } else { + const checkoutTransaction = getCheckoutTransaction(); if ( - getItem(StorageItem.CheckoutTransaction) && + checkoutTransaction && + checkoutTransaction.transactionId && (!this.subscriptionTransaction$ || this.subscriptionTransaction$.closed) ) { this.subscriptionTransaction$ = this.orderApi - .listen(getItem(StorageItem.CheckoutTransaction)) + .listen(checkoutTransaction.transactionId) .pipe(untilDestroyed(this)) - .subscribe(this.transaction$); + .subscribe((transaction) => { + this.transaction$.next(transaction); + }); } } }); @@ -236,60 +240,73 @@ export class HeaderComponent implements OnInit, OnDestroy { } }); - this.cartService.showCart$.subscribe((value) => { - // console.log('Current value of showCart$: ', value); + this.cartItemsSubscription$ = this.cartService.getCartItems().subscribe((items) => { + this.cartItemCount = items.length; + }); + + this.cartService.checkoutOverlayOpen$.pipe(untilDestroyed(this)).subscribe((isOpen) => { + this.isCheckoutOverlayOpen = isOpen; + }); + + this.cartService.cartModalOpen$.pipe(untilDestroyed(this)).subscribe((isOpen) => { + this.isCartCheckoutOpen = isOpen; }); } public async onOpenCheckout(): Promise { - const t = this.transaction$.getValue(); - console.log('[header-onOpenCheckout] transaction: ', t); - console.log('[header-onOpenCheckout] t?.payload.type: ', t?.payload.type); - if (t?.payload.type == TransactionPayloadType.NFT_PURCHASE_BULK) { - console.log( - '[header-onOpenCheckout] !t?.payload.type && t?.payload.type == TransactionPayloadType.NFT_PURCHASE_BULK equals true and isCartCheckoutOpen set to true', - ); - this.openCartModal.emit(); - this.openCheckoutOverlay(); - } + const checkoutTransaction = getCheckoutTransaction(); + if (checkoutTransaction) { + switch (checkoutTransaction.source) { + case 'cartCheckout': { + if (!this.cartService.isCheckoutOverlayOpen()) { + this.cartService.openCartAndCheckoutOverlay(); + this.cd.markForCheck(); + } + break; + } + case 'nftCheckout': { + const t = this.transaction$.getValue(); - if (!t?.payload.nft || !t.payload.collection) { - return; - } - const collection: Collection | undefined = await firstValueFrom( - this.collectionApi.listen(t?.payload.collection), - ); - 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 - if (collection?.placeholderNft) { - nft = await firstValueFrom(this.nftApi.listen(collection?.placeholderNft)); + if (!t?.payload.nft || !t.payload.collection) { + return; + } + + const collection: Collection | undefined = await firstValueFrom( + this.collectionApi.listen(t?.payload.collection), + ); + + let nft: Nft | undefined = undefined; + try { + nft = await firstValueFrom(this.nftApi.listen(t?.payload?.nft)); + } catch (_e) { + if (collection?.placeholderNft) { + nft = await firstValueFrom(this.nftApi.listen(collection?.placeholderNft)); + } + } + + if (nft && collection) { + this.currentCheckoutCollection = collection; + this.currentCheckoutNft = nft; + this.isCheckoutOpen = true; + this.cd.markForCheck(); + } + break; + } + default: { + console.error('Unknown checkout transaction source:', checkoutTransaction.source); + } } - } - if (nft && collection) { - this.currentCheckoutCollection = collection; - this.currentCheckoutNft = nft; - this.isCheckoutOpen = true; - this.cd.markForCheck(); + } else { + this.removeCheckoutNotification(); } } - private openCheckoutOverlay(): void { - const cartItems = this.cartService.getCartItems().getValue(); - - this.modalService.create({ - nzTitle: 'Checkout', - nzContent: CheckoutOverlayComponent, - nzComponentParams: { items: cartItems }, - nzFooter: null, - nzWidth: '80%', - }); + public handleOpenCartModal(): void { + this.cartService.showCartModal(); } - public handleOpenCartModal(): void { - this.openCartModal.emit(); + public handleOpenCartCheckoutModal(): void { + this.cartService.openCheckoutOverlay(); } public get filesizes(): typeof FILE_SIZES { @@ -308,15 +325,6 @@ export class HeaderComponent implements OnInit, OnDestroy { return '/' + ROUTER_UTILS.config.market.root; } - public closeCheckout(): void { - this.checkoutService.modalOpen$.next(false); - this.isCheckoutOpen = false; - } - - public closeCartCheckout() { - this.isCartCheckoutOpen = false; - } - public goToMyProfile(): void { if (this.member$.value?.uid) { this.router.navigate([ @@ -336,6 +344,11 @@ export class HeaderComponent implements OnInit, OnDestroy { return item.uid; } + public closeCheckout(): void { + this.checkoutService.modalOpen$.next(false); + this.isCheckoutOpen = false; + } + private removeCheckoutNotification(removeFromStorage = true): void { if (this.notificationRef) { this.nzNotification.remove(this.notificationRef.messageId); @@ -427,24 +440,14 @@ export class HeaderComponent implements OnInit, OnDestroy { }); } - public openShoppingCart(): void { - // console.log('Opening shopping cart...'); - this.cartService.showCart(); - } - - public handleCartCheckout(): void { - this.isCartCheckoutOpen = true; - this.cd.markForCheck(); - } - public ngOnDestroy(): void { this.cancelAccessSubscriptions(); this.subscriptionNotification$?.unsubscribe(); this.subscriptionTransaction$?.unsubscribe(); this.currentCheckoutNft = undefined; this.currentCheckoutCollection = undefined; - if (this.cartItemsSubscription) { - this.cartItemsSubscription.unsubscribe(); + if (this.cartItemsSubscription$) { + this.cartItemsSubscription$.unsubscribe(); } } } diff --git a/src/app/components/cart/cart.module.ts b/src/app/components/cart/cart.module.ts index f503b60..00b113e 100644 --- a/src/app/components/cart/cart.module.ts +++ b/src/app/components/cart/cart.module.ts @@ -6,8 +6,6 @@ import { CartModalComponent } from './components/cart-modal/cart-modal.component import { IconModule } from '@components/icon/icon.module'; import { NzNotificationModule } from 'ng-zorro-antd/notification'; import { FormatTokenModule } from '@core/pipes/formatToken/format-token.module'; -// import { NftRoutingModule } from '@pages/nft/nft-routing.module'; -// import { CollectionRoutingModule } from '@pages/collection/collection-routing.module'; import { CheckoutOverlayComponent } from './components/checkout/checkout-overlay.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; @@ -25,6 +23,8 @@ import { NetworkModule } from '@components/network/network.module'; import { RouterModule } from '@angular/router'; import { WalletDeeplinkModule } from '@components/wallet-deeplink/wallet-deeplink.module'; import { NzRadioModule } from 'ng-zorro-antd/radio'; +import { UsdBelowTwoDecimalsModule } from '@core/pipes/usd-below-two-decimals/usd-below-two-decimals.module'; +import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; @NgModule({ declarations: [CartModalComponent, CheckoutOverlayComponent], @@ -35,8 +35,6 @@ import { NzRadioModule } from 'ng-zorro-antd/radio'; IconModule, NzNotificationModule, FormatTokenModule, - // NftRoutingModule, - // CollectionRoutingModule, FormsModule, NzInputNumberModule, ReactiveFormsModule, @@ -54,6 +52,8 @@ import { NzRadioModule } from 'ng-zorro-antd/radio'; RouterModule, WalletDeeplinkModule, NzRadioModule, + UsdBelowTwoDecimalsModule, + NzToolTipModule, ], exports: [CartModalComponent, CheckoutOverlayComponent], }) diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.html b/src/app/components/cart/components/cart-modal/cart-modal.component.html index ad1d1d0..501e62e 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.html +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.html @@ -1,35 +1,79 @@ -
+
+
-
+
+
+ +
+
+
+
Info
+
+
+ + +
+
Status
+
+ + +
+
+ Qty Added / Available +
+
+ + +
+
+ Price Each (USD Value) +
+
+ + +
+
Remove
+
+
+
+
{{ item.nft.name }}
-
- Info -
+
Collection: Your Cart

-
- Status -
- {{ cartItemsStatus[i] }} + + + {{ cartItemStatus(item).status }} + + + + {{ cartItemStatus(item).status }} + +
-
- Qty Added / Available -
+
Your Cart

(change)="updateQuantity($event, item.nft.uid)" min="0" max="{{ cartItemsQuantities[i] }}" + [disabled]="item.quantity < 2" /> / {{ cartItemsQuantities[i] }} @@ -91,9 +141,7 @@
-
- Price Each -
+
Your Cart : true | async }} +   ({{ + unitsService.getUsd( + cartItemPrices[item.nft.uid].originalPrice, + cartItemPrices[item.nft.uid].tokenSymbol + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }} + USD)
{{ @@ -121,6 +179,16 @@ : true | async }} +   ({{ + unitsService.getUsd( + cartItemPrices[item.nft.uid].discountedPrice, + cartItemPrices[item.nft.uid].tokenSymbol + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }} + USD)
@@ -129,10 +197,23 @@
-
- Remove -
-
+
+ Select Network Purchase Bulk purchase of + NFTs are only allowed on one network per transaction. Please select the network group of NFTs + you'd like to purchase below. You can click on the network to toggle visibility of the NFTs to + purchase for that network. After making your selection you can confirm and lock the group of + NFTs to generate the purchase transaction. +
-
-

Network/Token: {{ group.tokenSymbol }}

- - +
- - - NFT Name - Collection Name - Quantity Added - Price ({{ group.tokenSymbol }}) - Line Item Total - - - - - - - - {{ item.nft.name }} - - - - - {{ item.collection.name }} - - - {{ item.quantity }} - - {{ - item.salePrice - | formatToken - : (item.nft.placeholderNft - ? item.collection.mintingData?.network - : item.nft.mintingData?.network) - : true - : true - | async - }} - - - {{ - item.quantity * item.salePrice - | formatToken - : (item.nft.placeholderNft - ? item.collection.mintingData?.network - : item.nft.mintingData?.network) - : true - : true - | async - }} - - - - - - Total - - {{ group.totalQuantity }} - - - - - {{ group.totalPrice | formatToken : group.network : true : true | async }} - - - - - +
+
+

+ Network: {{ group.tokenSymbol }} - + {{ group.items.length }} NFTs in network group. +

+
+
+ +
+
+ + + +
+ + + + NFT Name + Collection Name + Royalties Fee + Quantity Added + + Price ({{ group.tokenSymbol }}) (USD Value) + + Line Item Total (USD Value) + + + + + + + + {{ item.nft.name }} + + + + + {{ item.collection.name }} + + + {{ (item.collection.royaltiesFee || 0) * 100 }}% + {{ item.quantity }} + + {{ + item.salePrice + | formatToken + : (item.nft.placeholderNft + ? item.collection.mintingData?.network + : item.nft.mintingData?.network) + : true + : true + | async + }} +   ({{ + unitsService.getUsd(item.salePrice, group.network) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }} + USD) + + + {{ + item.quantity * item.salePrice + | formatToken + : (item.nft.placeholderNft + ? item.collection.mintingData?.network + : item.nft.mintingData?.network) + : true + : true + | async + }} +   ({{ + unitsService.getUsd(item.quantity * item.salePrice, group.network) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }} + USD) + + + + + + + Total + + {{ group.totalQuantity }} + + + + + {{ group.totalPrice | formatToken : group.network : true : true | async }} + + + + + +
+
+ +
@@ -126,7 +184,7 @@

Network/Token: {{ group.tokenSy nzSize="large" class="w-full lg:mr-2" i18n - (click)="close(false)" + (click)="handleClose(false)" > Close checkout @@ -283,7 +341,7 @@

Network/Token: {{ group.tokenSy nzSize="large" class="w-full lg:mr-2 mb-2" i18n - (click)="close()" + (click)="handleClose(false)" > Close checkout diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.less b/src/app/components/cart/components/checkout/checkout-overlay.component.less index e69de29..2739106 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.less +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.less @@ -0,0 +1,17 @@ +.radio-option { + padding: 0.75rem; + border: 4px solid transparent; + cursor: pointer; +} + +.radio-option:hover { + border-color: rgb(217, 75, 8); +} + +.radio-option.selected { + border-color: rgb(217, 75, 8); +} + +.radio-option input[type='radio'] { + display: none; +} diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.ts b/src/app/components/cart/components/checkout/checkout-overlay.component.ts index a1ded8b..ded5948 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.ts +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.ts @@ -6,8 +6,9 @@ import { ChangeDetectionStrategy, Output, EventEmitter, + OnDestroy, } from '@angular/core'; -import { CartItem, CartService } from './../../services/cart.service'; +import { CartItem, CartService } from '@components/cart/services/cart.service'; import { CollectionType, Nft, @@ -17,23 +18,28 @@ import { TRANSACTION_AUTO_EXPIRY_MS, NftPurchaseRequest, Network, + DEFAULT_NETWORK, } from '@build-5/interfaces'; import { AuthService } from '@components/auth/services/auth.service'; import { NotificationService } from '@core/services/notification'; import { OrderApi } from '@api/order.api'; import { NftApi } from '@api/nft.api'; -// import { FileApi } from '@api/file.api'; import { BehaviorSubject, firstValueFrom, interval, Subscription } from 'rxjs'; import { TransactionService } from '@core/services/transaction'; -import { getItem, removeItem, setItem, StorageItem } from '@core/utils'; +import { + removeItem, + StorageItem, + setCheckoutTransaction, + getCheckoutTransaction, +} from '@core/utils'; import dayjs from 'dayjs'; import { HelperService } from '@pages/nft/services/helper.service'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { ROUTER_UTILS } from '@core/utils/router.utils'; import { Router } from '@angular/router'; import { NzNotificationService } from 'ng-zorro-antd/notification'; -import { NzModalRef } from 'ng-zorro-antd/modal'; import { ThemeList, ThemeService } from '@core/services/theme'; +import { UnitsService } from '@core/services/units/units.service'; export enum StepType { CONFIRM = 'Confirm', @@ -65,19 +71,17 @@ interface HistoryItem { styleUrls: ['./checkout-overlay.component.less'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CheckoutOverlayComponent implements OnInit { +export class CheckoutOverlayComponent implements OnInit, OnDestroy { @Input() currentStep = StepType.CONFIRM; @Input() items: CartItem[] = []; - @Input() set isOpen(value: boolean) { - this._isOpen = value; - // this.checkoutService.modalOpen$.next(value); - } + @Input() pendingTransaction: Transaction | undefined; @Output() wenOnClose = new EventEmitter(); @Output() wenOnCloseCartCheckout = new EventEmitter(); - groupedCartItems: GroupedCartItem[] = []; - unavailableItemCount = 0; - cartItemPrices: { [key: string]: { originalPrice: number; discountedPrice: number } } = {}; + + public groupedCartItems: GroupedCartItem[] = []; + public unavailableItemCount = 0; + public cartItemPrices: { [key: string]: { originalPrice: number; discountedPrice: number } } = {}; public agreeTermsConditions = false; public transaction$: BehaviorSubject = new BehaviorSubject< Transaction | undefined @@ -90,20 +94,20 @@ export class CheckoutOverlayComponent implements OnInit { public targetAddress?: string; public targetAmount?: number; public purchasedNfts?: Nft[] | null; - private _isOpen = false; public stepType = StepType; public selectedNetwork: string | null = null; public mintingDataNetwork: Network | undefined; - formattedTotalPrice = ''; + public formattedTotalPrice = ''; private purchasedTokenSymbol: string | null = null; - - private transSubscription?: Subscription; + private transSubscription$?: Subscription; public nftPath = ROUTER_UTILS.config.nft.root; public collectionPath: string = ROUTER_UTILS.config.collection.root; + public expandedGroups = new Set(); + private currentTransactionSubscription?: Subscription; public theme = ThemeList; constructor( - private cartService: CartService, + public cartService: CartService, private auth: AuthService, private notification: NotificationService, private orderApi: OrderApi, @@ -113,8 +117,8 @@ export class CheckoutOverlayComponent implements OnInit { private nftApi: NftApi, private router: Router, private nzNotification: NzNotificationService, - private modalRef: NzModalRef, public themeService: ThemeService, + public unitsService: UnitsService, ) {} public get themes(): typeof ThemeList { @@ -122,23 +126,24 @@ export class CheckoutOverlayComponent implements OnInit { } ngOnInit() { - // console.log('checkout-overlay ngOnInit called, running groupItems code.'); + this.subscribeToCurrentStep(); + this.subscribeToCurrentTransaction(); + this.subscribeToCartItems(); + this.groupItems(); this.receivedTransactions = false; const listeningToTransaction: string[] = []; this.transaction$.pipe(untilDestroyed(this)).subscribe((val) => { - // console.log('transaction val: ', val); if (val && val.type === TransactionType.ORDER) { this.targetAddress = val.payload.targetAddress; this.targetAmount = val.payload.amount; const expiresOn: dayjs.Dayjs = dayjs(val.payload.expiresOn!.toDate()); if (expiresOn.isBefore(dayjs()) || val.payload?.void || val.payload?.reconciled) { - // It's expired. removeItem(StorageItem.CheckoutTransaction); } if (val.linkedTransactions && val.linkedTransactions?.length > 0) { this.currentStep = StepType.WAIT; - // Listen to other transactions. + this.updateStep(this.currentStep); for (const tranId of val.linkedTransactions) { if (listeningToTransaction.indexOf(tranId) > -1) { continue; @@ -152,6 +157,7 @@ export class CheckoutOverlayComponent implements OnInit { } } else if (!val.linkedTransactions || val.linkedTransactions.length === 0) { this.currentStep = StepType.TRANSACTION; + this.updateStep(this.currentStep); } this.expiryTicker$.next(expiresOn); @@ -165,6 +171,10 @@ export class CheckoutOverlayComponent implements OnInit { $localize`Payment received.`, (val).payload?.chainReference, ); + + this.currentStep = StepType.COMPLETE; + this.updateStep(this.currentStep); + this.cd.markForCheck(); } if ( @@ -173,7 +183,6 @@ export class CheckoutOverlayComponent implements OnInit { val.payload.reconciled === true && (val).payload.invalidPayment === false ) { - // Let's add delay to achive nice effect. setTimeout(() => { this.pushToHistory( val, @@ -192,14 +201,11 @@ export class CheckoutOverlayComponent implements OnInit { ); this.receivedTransactions = true; this.currentStep = StepType.COMPLETE; - - // console.log('[checkout-overlay.component-purchase] transaction after purchase complete: ', val); + this.updateStep(this.currentStep); this.removePurchasedGroupItems(); - this.cd.markForCheck(); }, 2000); - // Load purchased NFTs. if (val.payload.nftOrders && val.payload.nftOrders.length > 0) { this.purchasedNfts = this.purchasedNfts || []; val.payload.nftOrders.forEach((nftOrder) => { @@ -233,6 +239,7 @@ export class CheckoutOverlayComponent implements OnInit { const markInvalid = () => { setTimeout(() => { this.currentStep = StepType.TRANSACTION; + this.updateStep(this.currentStep); this.invalidPayment = true; this.cd.markForCheck(); }, 2000); @@ -268,26 +275,28 @@ export class CheckoutOverlayComponent implements OnInit { val.payload?.walletReference?.chainReference, ); - // Let's go back to wait. With slight delay so they can see this. markInvalid(); } this.cd.markForCheck(); }); - if (getItem(StorageItem.CheckoutTransaction)) { - this.transSubscription = this.orderApi - .listen(getItem(StorageItem.CheckoutTransaction)) - .subscribe(this.transaction$); + const checkoutTransaction = getCheckoutTransaction(); + if (checkoutTransaction && checkoutTransaction.transactionId) { + this.transSubscription$ = this.orderApi + .listen(checkoutTransaction.transactionId) + .subscribe((transaction) => { + this.transaction$.next(transaction); + }); + } else { + this.transSubscription$?.unsubscribe(); } - // Run ticker. const int: Subscription = interval(1000) .pipe(untilDestroyed(this)) .subscribe(() => { this.expiryTicker$.next(this.expiryTicker$.value); - // If it's in the past. if (this.expiryTicker$.value) { const expiresOn: dayjs.Dayjs = dayjs(this.expiryTicker$.value).add( TRANSACTION_AUTO_EXPIRY_MS, @@ -301,16 +310,105 @@ export class CheckoutOverlayComponent implements OnInit { } } }); + + if (this.currentStep === StepType.CONFIRM) { + this.clearNetworkSelection(); + + if (this.groupedCartItems.length === 1) { + this.setNetworkSelection(this.groupedCartItems[0].tokenSymbol); + } + } else { + const storedNetwork = localStorage.getItem('cartCheckoutSelectedNetwork'); + if (storedNetwork) { + this.selectedNetwork = storedNetwork; + } + } + this.setDefaultGroupVisibility(); } - groupItems() { - // console.log('groupItems function called.') + private subscribeToCartItems() { + this.cartService + .getCartItems() + .pipe(untilDestroyed(this)) + .subscribe((cartItems) => { + this.items = cartItems; + this.groupItems(); + this.cd.markForCheck(); + }); + } + + private subscribeToCurrentStep() { + this.cartService.currentStep$.pipe(untilDestroyed(this)).subscribe((step) => { + this.currentStep = step; + this.setDefaultGroupVisibility(); + this.cd.markForCheck(); + }); + } + + private subscribeToCurrentTransaction() { + const fetchedCurrentTransaction = this.cartService.getCurrentTransaction(); + + if (fetchedCurrentTransaction === undefined) { + return; + } + + this.currentTransactionSubscription = this.cartService + .getCurrentTransaction() + .pipe(untilDestroyed(this)) + .subscribe((transaction) => { + this.pendingTransaction = transaction; + this.cd.markForCheck(); + }); + } + + public setDefaultGroupVisibility() { + if (this.currentStep !== this.stepType.CONFIRM && this.selectedNetwork) { + this.expandedGroups.add(this.selectedNetwork); + this.expandedGroups.clear(); + } else { + if (this.currentStep === this.stepType.CONFIRM && this.groupedCartItems.length === 1) { + this.groupedCartItems.forEach((group) => { + this.expandedGroups.add(group.tokenSymbol); + }); + } + } + + this.cd.markForCheck(); + } + + public toggleGroup(groupSymbol: string) { + if (this.expandedGroups.has(groupSymbol)) { + this.expandedGroups.delete(groupSymbol); + } else { + this.expandedGroups.add(groupSymbol); + } + this.cd.markForCheck(); + } + + public isGroupExpanded(groupSymbol: string): boolean { + return this.expandedGroups.has(groupSymbol); + } + + public setNetworkSelection(networkSymbol: string): void { + this.selectedNetwork = networkSymbol; + localStorage.setItem('cartCheckoutSelectedNetwork', networkSymbol); + this.expandedGroups.clear(); + this.expandedGroups.add(networkSymbol); + this.cd.markForCheck(); + } + + public clearNetworkSelection(): void { + localStorage.removeItem('cartCheckoutSelectedNetwork'); + this.selectedNetwork = null; + } + + public groupItems() { const groups: { [tokenSymbol: string]: GroupedCartItem } = {}; this.items.forEach((item) => { const tokenSymbol = (item.nft?.placeholderNft ? item.collection?.mintingData?.network - : item.nft?.mintingData?.network) || 'Unknown'; + : item.nft?.mintingData?.network) || DEFAULT_NETWORK; const discount = this.discount(item); const originalPrice = this.calcPrice(item, 1); const discountedPrice = this.calcPrice(item, discount); @@ -320,9 +418,9 @@ export class CheckoutOverlayComponent implements OnInit { const network = (item.nft?.placeholderNft ? item.collection?.mintingData?.network - : item.nft?.mintingData?.network) || undefined; + : item.nft?.mintingData?.network) || DEFAULT_NETWORK; - if (this.cartService.isCartItemAvailableForSale(item)) { + if (this.cartService.isCartItemAvailableForSale(item).isAvailable) { if (!groups[tokenSymbol]) { groups[tokenSymbol] = { tokenSymbol, @@ -338,21 +436,19 @@ export class CheckoutOverlayComponent implements OnInit { } else { this.unavailableItemCount++; } - - // console.log('Cart item loop finished, group: ', groups[tokenSymbol]) }); this.groupedCartItems = Object.values(groups); + } - if (this.groupedCartItems.length === 1) { - this.selectedNetwork = this.groupedCartItems[0].tokenSymbol; - } + public updateStep(step: StepType) { + this.cartService.setCurrentStep(step); } private removePurchasedGroupItems(): void { - if (this.purchasedTokenSymbol) { - this.cartService.removeGroupItemsFromCart(this.purchasedTokenSymbol); - this.purchasedTokenSymbol = null; + if (this.selectedNetwork) { + this.cartService.removeGroupItemsFromCart(this.selectedNetwork); + this.clearNetworkSelection(); } } @@ -365,40 +461,40 @@ export class CheckoutOverlayComponent implements OnInit { } public isCartItemAvailableForSale(item: CartItem): any { - return this.cartService.isCartItemAvailableForSale(item); + return this.cartService.isCartItemAvailableForSale(item).isAvailable; } public reset(): void { this.receivedTransactions = false; - this.isOpen = false; this.currentStep = StepType.CONFIRM; this.purchasedNfts = undefined; + this.history = []; this.cd.markForCheck(); } - public close(alsoCloseCartModal = false): void { - this.wenOnCloseCartCheckout.emit(alsoCloseCartModal); - this.modalRef.close(); + public handleClose(alsoCloseCartModal = false): void { + this.cartService.closeCheckoutOverlay(); + if (alsoCloseCartModal) { + this.cartService.hideCartModal(); + } } - public goToNft(nftUid: string, alsoCloseCartModal = false): void { + public goToNft(nftUid: string): void { if (!nftUid) { - console.error('No NFT UID provided.'); return; } + this.router.navigate(['/', this.nftPath, nftUid]); - this.wenOnCloseCartCheckout.emit(alsoCloseCartModal); - this.modalRef.close(); + this.handleClose(true); } - public goToCollection(colUid: string, alsoCloseCartModal = false): void { + public goToCollection(colUid: string): void { if (!colUid) { - console.error('No Collection UID provided.'); return; } + this.router.navigate(['/', this.collectionPath, colUid]); - this.wenOnCloseCartCheckout.emit(alsoCloseCartModal); - this.modalRef.close(); + this.handleClose(true); } public getRecords(): Nft[] | null | undefined { @@ -436,6 +532,14 @@ export class CheckoutOverlayComponent implements OnInit { } public async initiateBulkOrder(): Promise { + if (this.cartService.hasPendingTransaction()) { + this.nzNotification.error( + 'You currently have an open order. Pay for it or let it expire.', + '', + ); + return; + } + const selectedGroup = this.groupedCartItems.find( (group) => group.tokenSymbol === this.selectedNetwork, ); @@ -449,7 +553,6 @@ export class CheckoutOverlayComponent implements OnInit { } if (!selectedGroup || selectedGroup.items.length === 0) { - console.warn('No network selected or no items in the selected network.'); this.nzNotification.error( $localize`No network selected or no items in the selected network.`, '', @@ -460,7 +563,6 @@ export class CheckoutOverlayComponent implements OnInit { const nfts = this.convertGroupedCartItemsToNfts(selectedGroup); if (nfts.length === 0) { - console.warn('No NFTs to purchase.'); this.nzNotification.error($localize`No NFTs to purchase.`, ''); return; } @@ -473,16 +575,12 @@ export class CheckoutOverlayComponent implements OnInit { selectedGroup.items.forEach((item) => { if (item.nft && item.collection) { - // console.log('[checkout-overlay.component-convertGroupedCartItemsToNfts] looped nft (item): ', item); - for (let i = 0; i < item.quantity; i++) { - // console.log('[checkout-overlay.component-convertGroupedCartItemsToNfts] creating bulk order NftPurchaseRequest for item, qty#: ', i) const nftData: NftPurchaseRequest = { collection: item.collection.uid, }; if (item.nft.owner || item.collection.type === CollectionType.CLASSIC) { - // console.log('[checkout-overlay.component-convertGroupedCartItemsToNfts] passed item.nft.owner || item.collection.type === CollectionType.CLASSIC. item.nft.owner: ' + item.nft.owner + '. item.collection.type: ' + item.collection.type + '. item.nft.uid: ' + item.nft.uid); nftData.nft = item.nft.uid; } @@ -495,12 +593,10 @@ export class CheckoutOverlayComponent implements OnInit { } public async proceedWithBulkOrder(nfts: NftPurchaseRequest[]): Promise { - // console.log('[checkout-overlay.component-proceddWithBulkOrder] nfts passed in: ', nfts) const selectedGroup = this.groupedCartItems.find( (group) => group.tokenSymbol === this.selectedNetwork, ); if (!selectedGroup) { - console.warn('No network selected or no items in the selected network.'); this.nzNotification.error( $localize`No network selected or no items in the selected network.`, '', @@ -509,7 +605,6 @@ export class CheckoutOverlayComponent implements OnInit { } if (nfts.length === 0 || !this.agreeTermsConditions) { - console.warn('No NFTs to purchase or terms and conditions are not agreed.'); this.nzNotification.error( $localize`No NFTs to purchase or terms and conditions are not agreed.`, '', @@ -521,8 +616,6 @@ export class CheckoutOverlayComponent implements OnInit { orders: nfts, }; - // console.log('[checkout-overlay.component-proceddWithBulkOrder] params being passed for signing: ', bulkPurchaseRequest); - await this.auth.sign(bulkPurchaseRequest, async (signedRequest, finish) => { this.notification .processRequest( @@ -532,9 +625,16 @@ export class CheckoutOverlayComponent implements OnInit { ) .subscribe((transaction: Transaction | undefined) => { if (transaction) { - this.transSubscription?.unsubscribe(); - setItem(StorageItem.CheckoutTransaction, transaction.uid); - this.transSubscription = this.orderApi + this.transSubscription$?.unsubscribe(); + setCheckoutTransaction({ + transactionId: transaction.uid, + source: 'cartCheckout', + }); + this.cartService.setCurrentTransaction(transaction.uid); + this.cartService.setCurrentStep(StepType.TRANSACTION); + this.cartService.refreshCartItems(); + this.cd.markForCheck(); + this.transSubscription$ = this.orderApi .listen(transaction.uid) .subscribe(this.transaction$); this.pushToHistory( @@ -544,7 +644,6 @@ export class CheckoutOverlayComponent implements OnInit { $localize`Waiting for transaction...`, ); } else { - console.error('Transaction failed or did not return a valid transaction.'); this.nzNotification.error( $localize`Transaction failed or did not return a valid transaction.`, '', @@ -553,4 +652,8 @@ export class CheckoutOverlayComponent implements OnInit { }); }); } + + ngOnDestroy() { + this.currentTransactionSubscription?.unsubscribe(); + } } diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts index b4cc607..6991538 100644 --- a/src/app/components/cart/services/cart.service.ts +++ b/src/app/components/cart/services/cart.service.ts @@ -1,10 +1,27 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; -import { Nft, Collection, MIN_AMOUNT_TO_TRANSFER } from '@build-5/interfaces'; -import { getItem, setItem, StorageItem } from './../../../@core/utils/local-storage.utils'; +import { BehaviorSubject, Observable, Subscription, map } from 'rxjs'; +import { + Nft, + Collection, + Transaction, + MIN_AMOUNT_TO_TRANSFER, + TRANSACTION_AUTO_EXPIRY_MS, + DEFAULT_NETWORK, + Space, + Award, + CollectionStatus, +} from '@build-5/interfaces'; +import { getItem, setItem, removeItem, StorageItem, getCheckoutTransaction } from '@core/utils'; import { NzNotificationService } from 'ng-zorro-antd/notification'; import { HelperService } from '@pages/nft/services/helper.service'; import { AuthService } from '@components/auth/services/auth.service'; +import { NzModalRef, NzModalService } from 'ng-zorro-antd/modal'; +import { CheckoutOverlayComponent } from '../components/checkout/checkout-overlay.component'; +import { OrderApi } from '@api/order.api'; +import { SpaceApi } from '@api/space.api'; +import { MemberApi } from '@api/member.api'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import dayjs from 'dayjs'; export interface CartItem { nft: Nft; @@ -13,48 +30,337 @@ export interface CartItem { salePrice: number; } +export enum StepType { + CONFIRM = 'Confirm', + TRANSACTION = 'Transaction', + WAIT = 'Wait', + COMPLETE = 'Complete', +} + @Injectable({ providedIn: 'root', }) +@UntilDestroy() export class CartService { - private showCartSubject = new BehaviorSubject(false); - public showCart$ = this.showCartSubject.asObservable(); - private cartItemsSubject = new BehaviorSubject(this.loadCartItems()); + private cartItemsSubject$ = new BehaviorSubject(this.loadCartItems()); + private cartModalOpenSubject$ = new BehaviorSubject(false); + public cartModalOpen$ = this.cartModalOpenSubject$.asObservable(); + public checkoutOverlayOpenSubject$ = new BehaviorSubject(false); + public checkoutOverlayOpen$ = this.checkoutOverlayOpenSubject$.asObservable(); + private currentStepSubject$ = new BehaviorSubject(StepType.CONFIRM); + public currentStep$ = this.currentStepSubject$.asObservable(); + private checkoutOverlayModalRef: NzModalRef | null = null; + private pendingTransaction$: BehaviorSubject = new BehaviorSubject< + Transaction | undefined + >(undefined); + private memberSpaces: string[] = []; + private memberGuardianSpaces: string[] = []; + private memberAwards: string[] = []; + private guardianSpaceSubscriptions$: { [key: string]: Subscription } = {}; constructor( private notification: NzNotificationService, private helperService: HelperService, public auth: AuthService, - ) {} + private modalService: NzModalService, + private orderApi: OrderApi, + private spaceApi: SpaceApi, + private memberApi: MemberApi, + ) { + this.subscribeToMemberChanges(); + } - public showCart(): void { - this.showCartSubject.next(true); + private subscribeToMemberChanges() { + this.auth.member$.pipe(untilDestroyed(this)).subscribe((member) => { + if (member) { + this.refreshMemberData(); + } else { + this.resetMemberData(); + } + }); } - public hideCart(): void { - this.showCartSubject.next(false); + private refreshMemberData() { + if (this.auth.member$.value?.uid) { + this.loadMemberSpaces(this.auth.member$.value?.uid); + this.loadMemberAwards(this.auth.member$.value?.uid); + } else { + this.resetMemberData(); + } } - public refreshCartItems(): void { - this.cartItemsSubject.next(this.cartItemsSubject.value); + private resetMemberData() { + this.memberSpaces = []; + this.memberGuardianSpaces = []; + this.memberAwards = []; + this.cleanupGuardianSubscriptions(); + } + + private loadMemberSpaces(memberId: string): void { + this.memberApi + .allSpacesAsMember(memberId) + .pipe(untilDestroyed(this)) + .subscribe((spaces) => { + if (spaces) { + this.memberSpaces = spaces.map((space) => space.uid); + this.updateMemberSpaces(this.memberSpaces); + } + }); + } + + private loadMemberAwards(memberId: string): void { + this.memberApi + .topAwardsCompleted(memberId) + .pipe(untilDestroyed(this)) + .subscribe((awards) => { + if (awards) { + this.memberAwards = awards.map((award) => award.uid); + this.updateMemberAwards(this.memberAwards); + } + }); + } + + private updateMemberSpaces(spaces: string[]) { + this.memberSpaces = spaces.map((space) => space); + this.memberGuardianSpaces = []; + spaces.forEach((space) => this.updateMemberGuardianStatus(space)); + } + + private updateMemberAwards(awards: string[]) { + this.memberAwards = awards.map((award) => award); + } + + private updateMemberGuardianStatus(spaceId: string) { + if (this.auth.member$.value?.uid) { + this.guardianSpaceSubscriptions$[spaceId]?.unsubscribe(); + this.guardianSpaceSubscriptions$[spaceId] = this.spaceApi + .isGuardianWithinSpace(spaceId, this.auth.member$.value?.uid) + .pipe(untilDestroyed(this)) + .subscribe((isGuardian) => { + if (isGuardian && !this.memberGuardianSpaces.includes(spaceId)) { + this.memberGuardianSpaces.push(spaceId); + } else if (!isGuardian && this.memberGuardianSpaces.includes(spaceId)) { + this.memberGuardianSpaces = this.memberGuardianSpaces.filter((id) => id !== spaceId); + } + }); + } + } + + private cleanupGuardianSubscriptions() { + Object.values(this.guardianSpaceSubscriptions$).forEach((subscription) => + subscription.unsubscribe(), + ); + this.guardianSpaceSubscriptions$ = {}; + } + + public hasPendingTransaction(): boolean { + const checkoutTransaction = getCheckoutTransaction(); + const transactionId = checkoutTransaction?.transactionId; + return !!transactionId; + } + + public setCurrentTransaction(transactionId: string): void { + if (transactionId === null || transactionId === undefined) { + return; + } + + this.orderApi + .listen(transactionId) + .pipe(untilDestroyed(this)) + .subscribe((transaction) => { + this.pendingTransaction$.next(transaction); + }); + } + + public getCurrentTransaction(): Observable { + return this.pendingTransaction$.asObservable(); + } + + public setCurrentStep(step: StepType): void { + this.currentStepSubject$.next(step); + } + + public getCurrentStep(): StepType { + return this.currentStepSubject$.getValue(); + } + + public showCartModal(): void { + this.cartModalOpenSubject$.next(true); + } + + public hideCartModal(): void { + this.cartModalOpenSubject$.next(false); + } + + public isCartModalOpen(): boolean { + return this.cartModalOpenSubject$.getValue(); + } + + public openCheckoutOverlay(): void { + if (!this.checkoutOverlayOpenSubject$.getValue()) { + const checkoutTransaction = getCheckoutTransaction(); + + if (checkoutTransaction && checkoutTransaction.transactionId) { + if (checkoutTransaction.source === 'nftCheckout') { + this.notification.error( + 'You currently have an open order. Pay for it or let it expire.', + '', + ); + return; + } + + if (checkoutTransaction.source === 'cartCheckout') { + this.setCurrentTransaction(checkoutTransaction.transactionId); + const currentTransaction = this.pendingTransaction$.getValue(); + + if (currentTransaction && currentTransaction.uid) { + const expiresOn: dayjs.Dayjs = dayjs(currentTransaction.createdOn!.toDate()).add( + TRANSACTION_AUTO_EXPIRY_MS, + 'ms', + ); + + if ( + expiresOn.isBefore(dayjs()) || + currentTransaction.payload?.void || + currentTransaction.payload?.reconciled + ) { + removeItem(StorageItem.CheckoutTransaction); + this.pendingTransaction$.next(undefined); + this.setCurrentStep(StepType.CONFIRM); + } + } + } else { + this.notification.error( + 'CheckoutTransaction exists and source is not nftCheckout, the checkout-overlay is not open and the pending transaction not expired or complete and the source is not cartCheckout, this should never happen.', + '', + ); + return; + } + } + + const cartItems = this.getCartItems().getValue(); + + this.checkoutOverlayModalRef = this.modalService.create({ + nzTitle: 'Checkout', + nzContent: CheckoutOverlayComponent, + nzComponentParams: { items: cartItems }, + nzFooter: null, + nzWidth: '80%', + }); + + this.checkoutOverlayModalRef.afterClose.subscribe(() => { + this.closeCheckoutOverlay(); + }); + + this.checkoutOverlayOpenSubject$.next(true); + } + } + + public closeCheckoutOverlay(): void { + const checkoutTransaction = getCheckoutTransaction(); + if (checkoutTransaction && checkoutTransaction.transactionId) { + if (checkoutTransaction.source === 'cartCheckout') { + this.setCurrentTransaction(checkoutTransaction.transactionId); + const currentTransaction = this.pendingTransaction$.getValue(); + if (this.checkoutOverlayModalRef) { + const currentStep = this.getCurrentStep(); + if (currentStep === StepType.TRANSACTION || currentStep === StepType.WAIT) { + if (currentTransaction && currentTransaction.uid) { + const expiresOn: dayjs.Dayjs = dayjs(currentTransaction.createdOn!.toDate()).add( + TRANSACTION_AUTO_EXPIRY_MS, + 'ms', + ); + + if ( + expiresOn.isBefore(dayjs()) || + currentTransaction.payload?.void || + currentTransaction.payload?.reconciled + ) { + removeItem(StorageItem.CheckoutTransaction); + this.pendingTransaction$.next(undefined); + this.setCurrentStep(StepType.CONFIRM); + } + } else { + removeItem(StorageItem.CheckoutTransaction); + this.pendingTransaction$.next(undefined); + this.setCurrentStep(StepType.CONFIRM); + } + + this.checkoutOverlayModalRef.close(); + this.checkoutOverlayOpenSubject$.next(false); + this.checkoutOverlayModalRef = null; + } else { + this.checkoutOverlayModalRef.close(); + this.checkoutOverlayOpenSubject$.next(false); + this.checkoutOverlayModalRef = null; + } + } else { + this.checkoutOverlayOpenSubject$.next(false); + } + } else { + if (this.checkoutOverlayModalRef) { + this.checkoutOverlayModalRef.close(); + this.checkoutOverlayOpenSubject$.next(false); + this.checkoutOverlayModalRef = null; + this.setCurrentStep(StepType.CONFIRM); + } + this.checkoutOverlayOpenSubject$.next(false); + } + } else { + if (this.checkoutOverlayModalRef) { + this.checkoutOverlayModalRef.close(); + this.checkoutOverlayOpenSubject$.next(false); + this.checkoutOverlayModalRef = null; + this.pendingTransaction$.next(undefined); + this.setCurrentStep(StepType.CONFIRM); + } + this.checkoutOverlayOpenSubject$.next(false); + } + } + + public isCheckoutOverlayOpen(): boolean { + return this.checkoutOverlayOpenSubject$.getValue(); + } + + public openCartAndCheckoutOverlay(): void { + const wasCartModalOpen = this.cartModalOpenSubject$.getValue(); + + if (!wasCartModalOpen) { + this.showCartModal(); + } + + if (!this.checkoutOverlayOpenSubject$.getValue()) { + if (!wasCartModalOpen) { + setTimeout(() => { + this.openCheckoutOverlay(); + }, 300); + } else { + this.openCheckoutOverlay(); + } + } + } + + public getCartItems(): BehaviorSubject { + return this.cartItemsSubject$; } public addToCart(cartItem: CartItem): void { - console.log('[CartService] addToCart function called. Adding cart item: ', cartItem); - const currentItems = this.cartItemsSubject.value; + const currentItems = this.cartItemsSubject$.value; + if (currentItems.length >= 100) { + this.notification.error( + $localize`Your cart is full. Please remove items before adding more.`, + '', + ); + return; + } const isItemAlreadyInCart = currentItems.some((item) => item.nft.uid === cartItem.nft.uid); - if (!isItemAlreadyInCart) { const updatedCartItems = [...currentItems, cartItem]; - this.cartItemsSubject.next(updatedCartItems); + this.cartItemsSubject$.next(updatedCartItems); this.saveCartItems(); this.notification.success( - $localize`NFT ` + - cartItem.nft.name + - ` from collection ` + - cartItem.collection.name + - ` has been added to your cart.`, + $localize`NFT ${cartItem.nft.name} from collection ${cartItem.collection.name} has been added to your cart.`, '', ); } else { @@ -62,66 +368,141 @@ export class CartService { } } - public removeFromCart(itemId: string): void { - // console.log('[CartService] removeFromCart function called.'); - const updatedCartItems = this.cartItemsSubject.value.filter((item) => item.nft.uid !== itemId); - this.cartItemsSubject.next(updatedCartItems); + public refreshCartItems(): void { + this.cartItemsSubject$.next(this.cartItemsSubject$.value); + } + + public removeFromCart(cartItem: CartItem): void { + const updatedCartItems = this.cartItemsSubject$.value.filter( + (item) => item.nft.uid !== cartItem.nft.uid, + ); + this.cartItemsSubject$.next(updatedCartItems); this.saveCartItems(); + this.notification.success( + $localize`NFT ${cartItem.nft.name} from collection ${cartItem.collection.name} has been removed from your cart.`, + '', + ); } public removeItemsFromCart(itemIds: string[]): void { - const updatedCartItems = this.cartItemsSubject.value.filter( + const updatedCartItems = this.cartItemsSubject$.value.filter( (item) => !itemIds.includes(item.nft.uid), ); - this.cartItemsSubject.next(updatedCartItems); + this.cartItemsSubject$.next(updatedCartItems); this.saveCartItems(); } public removeGroupItemsFromCart(tokenSymbol: string): void { - const updatedCartItems = this.cartItemsSubject.value.filter((item) => { + const updatedCartItems = this.cartItemsSubject$.value.filter((item) => { const itemTokenSymbol = (item.nft?.placeholderNft ? item.collection?.mintingData?.network - : item.nft?.mintingData?.network) || 'Unknown'; + : item.nft?.mintingData?.network) || DEFAULT_NETWORK; return itemTokenSymbol !== tokenSymbol; }); - this.cartItemsSubject.next(updatedCartItems); + this.cartItemsSubject$.next(updatedCartItems); this.saveCartItems(); } - public getCartItems(): BehaviorSubject { - return this.cartItemsSubject; - } + public isNftAvailableForSale( + nft: Nft, + collection: Collection, + ): { isAvailable: boolean; message: string } { + let message = 'NFT is available for sale.'; + const conditions: string[] = []; - public updateCartItems(updatedItems: CartItem[]): void { - this.cartItemsSubject.next(updatedItems); - this.saveCartItems(); - } + let isAvailable = false; - public saveCartItems(): void { - setItem(StorageItem.CartItems, this.cartItemsSubject.value); - } + if (!collection) { + message = 'Internal Error: Collection data is null or undefined.'; + return { isAvailable, message }; + } - private loadCartItems(): CartItem[] { - const items = getItem(StorageItem.CartItems) as CartItem[]; - return items || []; - } + if (!nft?.availableFrom) { + message = 'Internal Error: Nft and/or NFT Available From date is null or undefined.'; + return { isAvailable, message }; + } + + let validAvailableFromDate = + !!this.helperService.getDate(nft.availableFrom) && + dayjs(this.helperService.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's'); + if (!validAvailableFromDate) { + validAvailableFromDate = + !!this.helperService.getDate(collection.availableFrom) && + dayjs(this.helperService.getDate(collection.availableFrom)).isSameOrBefore(dayjs(), 's'); + } + if (!validAvailableFromDate) conditions.push('NFT does not have a valid "availableFrom" date.'); + + const collectionApproved = collection.approved; + if (!collectionApproved) conditions.push('Collection is not Approved.'); + + const collectionStatusMinting = collection?.status === CollectionStatus.MINTING; + if (collectionStatusMinting) conditions.push('Collection is in minting status.'); - public isNftAvailableForSale(nft: Nft, collection: Collection): boolean { const isLocked = this.helperService.isLocked(nft, collection, true); + if (isLocked) conditions.push('NFT is locked.'); + + const saleAccessMembersBlocked = + (nft?.saleAccessMembers?.length ?? 0) > 0 && + !nft?.saleAccessMembers?.includes(this.auth.member$.value?.uid ?? ''); + if (saleAccessMembersBlocked) conditions.push('Sale access is blocked for this member.'); let isOwner = false; if (nft.owner != null && this.auth.member$.value?.uid != null) { isOwner = nft.owner === this.auth.member$.value?.uid; } + if (isOwner) conditions.push('You are the owner of this NFT.'); + + const availableValue = +nft?.available; + const nftAvailable = availableValue === 1 || availableValue === 3; + if (!nftAvailable) conditions.push('NFT is not marked as available.'); + + const spaceMemberAccess = + collection?.access !== 1 || + (collection?.access === 1 && this.memberSpaces.includes(collection?.space ?? '')); + if (!spaceMemberAccess) conditions.push('Member does not have access to this space.'); + + const spaceGuardianAccess = + collection?.access !== 2 || + (collection?.access === 2 && this.memberGuardianSpaces.includes(collection?.space ?? '')); + if (!spaceGuardianAccess) conditions.push('Member is not a guardian of this space.'); - const availableForSale = this.helperService.isAvailableForSale(nft, collection); + const spaceAwardAccess = + collection?.access !== 3 || + (collection?.access === 3 && + collection?.accessAwards?.some((award) => this.memberAwards.includes(award))); + if (!spaceAwardAccess) conditions.push('Member does not have the required awards for access.'); + + isAvailable = + !collectionStatusMinting && + collectionApproved && + validAvailableFromDate && + !isLocked && + !saleAccessMembersBlocked && + (!isOwner || !nft.owner) && + nftAvailable && + spaceMemberAccess && + spaceGuardianAccess && + spaceAwardAccess; + + if (!isAvailable && conditions.length > 0) { + message = + 'NFT is not available for sale due to the following conditions: ' + conditions.join(' '); + } - return !isLocked && availableForSale && (!isOwner || !nft.owner); + return { isAvailable, message }; } - public isCartItemAvailableForSale(cartItem: CartItem): boolean { - return this.isNftAvailableForSale(cartItem.nft, cartItem.collection); + public clearCart(): void { + this.cartItemsSubject$.next([]); + this.saveCartItems(); + this.notification.success($localize`All items have been removed from your cart.`, ''); + } + + public isCartItemAvailableForSale(cartItem: CartItem): { isAvailable: boolean; message: string } { + const isAvailable = this.isNftAvailableForSale(cartItem.nft, cartItem.collection).isAvailable; + const message = this.isNftAvailableForSale(cartItem.nft, cartItem.collection).message; + return { isAvailable, message }; } public getAvailableNftQuantity(cartItem: CartItem): number { @@ -138,6 +519,20 @@ export class CartService { return 0; } + public updateCartItems(updatedItems: CartItem[]): void { + this.cartItemsSubject$.next(updatedItems); + this.saveCartItems(); + } + + public saveCartItems(): void { + setItem(StorageItem.CartItems, this.cartItemsSubject$.value); + } + + public loadCartItems(): CartItem[] { + const items = getItem(StorageItem.CartItems) as CartItem[]; + return items || []; + } + 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/components/nft/components/nft-card/nft-card.component.html b/src/app/components/nft/components/nft-card/nft-card.component.html index 1120b0f..febf5aa 100644 --- a/src/app/components/nft/components/nft-card/nft-card.component.html +++ b/src/app/components/nft/components/nft-card/nft-card.component.html @@ -239,24 +239,41 @@ - - - + + + + + + + + + +

diff --git a/src/app/components/nft/components/nft-card/nft-card.component.ts b/src/app/components/nft/components/nft-card/nft-card.component.ts index 72a82a6..4fa8cd1 100644 --- a/src/app/components/nft/components/nft-card/nft-card.component.ts +++ b/src/app/components/nft/components/nft-card/nft-card.component.ts @@ -37,13 +37,13 @@ export class NftCardComponent { @Input() set nft(value: Nft | null | undefined) { - if (this.memberApiSubscription) { - this.memberApiSubscription.unsubscribe(); + if (this.memberApiSubscription$) { + this.memberApiSubscription$.unsubscribe(); } this._nft = value; const owner = this.nft?.owner || this.nft?.createdBy; if (owner) { - this.memberApiSubscription = this.memberApi + this.memberApiSubscription$ = this.memberApi .listen(owner) .pipe(untilDestroyed(this)) .subscribe(this.owner$); @@ -76,7 +76,7 @@ export class NftCardComponent { public owner$: BehaviorSubject = new BehaviorSubject( undefined, ); - private memberApiSubscription?: Subscription; + private memberApiSubscription$?: Subscription; private _nft?: Nft | null; constructor( @@ -194,11 +194,8 @@ export class NftCardComponent { event.preventDefault(); if (nft && collection) { - console.log('[NftCardComponent] Adding item to cart:', nft, collection); this.cartService.addToCart({ nft, collection, quantity: 1, salePrice: 0 }); - // Optionally, provide feedback to the user } else { - // Handle the case when nft or collection is null or undefined console.error('Attempted to add a null or undefined NFT or Collection to the cart'); } } 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 afb7111..71cf5ce 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 @@ -20,7 +20,14 @@ import { NotificationService } from '@core/services/notification'; import { PreviewImageService } from '@core/services/preview-image'; import { TransactionService } from '@core/services/transaction'; import { UnitsService } from '@core/services/units'; -import { getItem, removeItem, setItem, StorageItem } from '@core/utils'; +import { + getItem, + removeItem, + setItem, + StorageItem, + setCheckoutTransaction, + getCheckoutTransaction, +} from '@core/utils'; import { ROUTER_UTILS } from '@core/utils/router.utils'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { HelperService } from '@pages/nft/services/helper.service'; @@ -137,7 +144,7 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { private _nft?: Nft | null; private _collection?: Collection | null; - private transSubscription?: Subscription; + private transSubscription$?: Subscription; public path = ROUTER_UTILS.config.nft.root; constructor( @@ -158,7 +165,6 @@ 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) => { @@ -168,6 +174,10 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { const expiresOn: dayjs.Dayjs = dayjs(val.payload.expiresOn!.toDate()); if (expiresOn.isBefore(dayjs()) || val.payload?.void || val.payload?.reconciled) { // It's expired. + console.log( + 'nft-checkout ngOnInit - transaction expired, has a void payload or transaction is reconciled, removing CheckoutTransaction, transaction: ', + val, + ); removeItem(StorageItem.CheckoutTransaction); } if (val.linkedTransactions && val.linkedTransactions?.length > 0) { @@ -307,10 +317,11 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { this.cd.markForCheck(); }); - if (getItem(StorageItem.CheckoutTransaction)) { - this.transSubscription = this.orderApi - .listen(getItem(StorageItem.CheckoutTransaction)) - .subscribe(this.transaction$); + const checkoutTransaction = getCheckoutTransaction(); + if (checkoutTransaction && checkoutTransaction.transactionId) { + this.transSubscription$ = this.orderApi + .listen(checkoutTransaction.transactionId) + .subscribe(this.transaction$); } // Run ticker. @@ -327,6 +338,10 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { ); if (expiresOn.isBefore(dayjs())) { this.expiryTicker$.next(null); + console.log( + 'nft-checkout ngOnInit - expiresOn.isBefore(dayjs()) passed, remove CheckoutTransaction, expiresOn: ', + expiresOn, + ); removeItem(StorageItem.CheckoutTransaction); int.unsubscribe(); this.reset(); @@ -430,27 +445,20 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { params.nft = this.nft.uid; } - // If owner is set CollectionType is not relevant. if (this.nft.owner) { params.nft = this.nft.uid; } - console.log( - '[nft-checkout.component-proceedWithOrder] params being passed for signing: ', - params, - ); - await this.auth.sign(params, (sc, finish) => { this.notification .processRequest(this.orderApi.orderNft(sc), $localize`Order created.`, finish) .subscribe((val: any) => { - console.log( - '[nft-this.checkoutService.component-proceedWithOrder] val after processing: ', - val, - ); - this.transSubscription?.unsubscribe(); - setItem(StorageItem.CheckoutTransaction, val.uid); - this.transSubscription = this.orderApi.listen(val.uid).subscribe(this.transaction$); + this.transSubscription$?.unsubscribe(); + setCheckoutTransaction({ + transactionId: val.uid, + source: 'nftCheckout', + }); + this.transSubscription$ = this.orderApi.listen(val.uid).subscribe(this.transaction$); this.pushToHistory(val, val.uid, dayjs(), $localize`Waiting for transaction...`); }); }); @@ -479,6 +487,6 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { } public ngOnDestroy(): void { - this.transSubscription?.unsubscribe(); + this.transSubscription$?.unsubscribe(); } } diff --git a/src/app/pages/collection/pages/collection/collection.page.ts b/src/app/pages/collection/pages/collection/collection.page.ts index 1ef8688..cd2c888 100644 --- a/src/app/pages/collection/pages/collection/collection.page.ts +++ b/src/app/pages/collection/pages/collection/collection.page.ts @@ -32,15 +32,7 @@ import { RANKING_TEST, } from '@build-5/interfaces'; import { NzNotificationService } from 'ng-zorro-antd/notification'; -import { - Subject, - BehaviorSubject, - first, - firstValueFrom, - skip, - Subscription, - takeUntil, -} from 'rxjs'; +import { Subject, BehaviorSubject, first, firstValueFrom, skip, Subscription } from 'rxjs'; import { DataService } from '../../services/data.service'; import { NotificationService } from './../../../../@core/services/notification/notification.service'; diff --git a/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts b/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts index 59010db..e38ce92 100644 --- a/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts +++ b/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts @@ -7,31 +7,26 @@ import { CartService } from '@components/cart/services/cart.service'; providedIn: 'root', }) export class CollectionNftStateService { - private listedNftsSubject = new BehaviorSubject([]); - public listedNfts$ = this.listedNftsSubject.asObservable(); - private availableNftsCountSubject = new BehaviorSubject(0); - public availableNftsCount$ = this.availableNftsCountSubject.asObservable(); + private listedNftsSubject$ = new BehaviorSubject([]); + public listedNfts$ = this.listedNftsSubject$.asObservable(); + private availableNftsCountSubject$ = new BehaviorSubject(0); + public availableNftsCount$ = this.availableNftsCountSubject$.asObservable(); constructor(private cartService: CartService) {} public setListedNfts(nfts: Nft[], collection: Collection) { - this.listedNftsSubject.next(nfts); + this.listedNftsSubject$.next(nfts); this.updateAvailableNftsCount(nfts, collection); } private updateAvailableNftsCount(nfts: Nft[], collection: Collection) { - console.log( - '[collectionNfts.service-updateAvailableNftsCount] function called with (nfts, collection): ', - nfts, - collection, - ); - const availableNftsCount = nfts.filter((nft) => - this.cartService.isNftAvailableForSale(nft, collection), + const availableNftsCount = nfts.filter( + (nft) => this.cartService.isNftAvailableForSale(nft, collection).isAvailable, ).length; - this.availableNftsCountSubject.next(availableNftsCount); + this.availableNftsCountSubject$.next(availableNftsCount); } public getListedNfts(): Nft[] { - return this.listedNftsSubject.getValue(); + return this.listedNftsSubject$.getValue(); } } diff --git a/src/app/pages/collection/pages/collection/nfts/nfts.page.ts b/src/app/pages/collection/pages/collection/nfts/nfts.page.ts index db9b7a6..94b28dd 100644 --- a/src/app/pages/collection/pages/collection/nfts/nfts.page.ts +++ b/src/app/pages/collection/pages/collection/nfts/nfts.page.ts @@ -81,12 +81,10 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { ) {} public ngOnInit(): void { - console.log('ngOnInit fires'); this.collectionNftStateService.availableNftsCount$ .pipe(takeUntil(this.destroy$)) .subscribe((count) => { this.availableNftsCount = count; - console.log('[ngOnInit] this.availableNftsCount is set to count: ', count); this.cd.markForCheck(); }); @@ -96,7 +94,6 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { } public ngOnChanges(changes: SimpleChanges): void { - console.log('ngOnChanges fires'); if (changes.collectionId) { this.resetComponentState(); if (this.collectionId) { @@ -106,20 +103,17 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { } private resetComponentState(): void { - console.log('resetComponentState fires'); this.availableNftsCount = 0; this.sweepCount = 1; this.cd.markForCheck(); } private loadCollection(collectionId: string): void { - console.log(`loadCollection fires for collectionId: ${collectionId}`); this.collectionApi .getCollectionById(collectionId) .pipe(take(1)) .subscribe({ next: (collectionData) => { - console.log('Full response from getCollectionById:', collectionData); if (collectionData) { this.collection = collectionData; const listedNfts = this.collectionNftStateService.getListedNfts(); @@ -136,9 +130,6 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { } private initializeAlgoliaFilters(collectionId: string): void { - console.log('initializeAlgoliaFilters fires with collectionId: ', collectionId); - - console.log('Current filters:', this.filterStorageService.marketNftsFilters$.value); this.filterStorageService.marketNftsFilters$.next({ ...this.filterStorageService.marketNftsFilters$.value, refinementList: { @@ -146,7 +137,6 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { collection: [collectionId], }, }); - console.log('Updated filters:', this.filterStorageService.marketNftsFilters$.value); this.config = { indexName: COL.NFT, @@ -160,12 +150,9 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { } public captureOriginalHits(hits: any[]) { - console.log('captureOriginalHits fires'); - // console.log('[nfts.page-captureOriginalHits] function called with following hits (hits, this.collection): ', hits, this.collection); if (hits && hits.length > 0 && this.collection) { this.originalNfts = hits; this.collectionNftStateService.setListedNfts(hits, this.collection); - // console.log('[nfts.page-captureOriginalHits] hits if passed, originalNfts set to hits and collectionNftStateService.setListedNfts given (hits, this.collection): ', hits, this.collection); } } @@ -174,19 +161,14 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { } public convertAllToSoonaverseModel = (algoliaItems: any[]) => { - console.log('convertAllToSoonaverseModel fires'); - // console.log('[nfts.page-convertAllToSoonaverseModel] function called with following algoliaItems: ', algoliaItems); if (this.originalNfts.length !== algoliaItems.length && algoliaItems.length > 0) { - // console.log('[nfts.page-convertAllToSoonaverseModel] run captureOriginalHits since aligoliaItems and originalNfts lengths dont match, (alogliaItems.length, originalNfts.length): ', algoliaItems.length, this.originalNfts.length); this.captureOriginalHits(algoliaItems); } - console.log('Before processing:', algoliaItems); const transformedItems = algoliaItems.map((algolia) => ({ ...algolia, availableFrom: Timestamp.fromMillis(+algolia.availableFrom), })); - console.log('After processing:', transformedItems); this.cd.markForCheck(); @@ -202,7 +184,6 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { } public sweepToCart(count: number) { - console.log('[nfts.page-sweepToCart] function called with following count: ', count); if (!this.collectionId) { this.notification.error($localize`Collection ID is not available.`, ''); return; @@ -215,17 +196,9 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { filter((collection): collection is Collection => collection !== undefined), switchMap((collection: Collection) => { const listedNfts = this.collectionNftStateService.getListedNfts(); - console.log( - '[nfts.page-sweepToCart] listedNfts set to this.collectionNftStateService: ', - listedNfts, - ); - const nftsForSale = listedNfts.filter((nft) => - this.cartService.isNftAvailableForSale(nft, collection), - ); - console.log( - '[nfts.page-sweepToCart] nftsForSale set to filtered listedNfts that pass true for "isNftAvailableForSale", nftsForSale: ', - nftsForSale, + const nftsForSale = listedNfts.filter( + (nft) => this.cartService.isNftAvailableForSale(nft, collection).isAvailable, ); const getEffectivePrice = (nft) => nft?.availablePrice || nft?.price || 0; @@ -235,17 +208,8 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { const priceB = getEffectivePrice(b); return priceA - priceB; }); - console.log( - '[nfts.page-sweepToCart] sortedNfts set to nftsToAdd sorted by "effective price" low to high, sortedNfts: ', - sortedNfts, - ); const nftsToAdd = sortedNfts.slice(0, Math.min(count, sortedNfts.length)); - console.log( - '[nfts.page-sweepToCart] nftsToAdd set to first N sorted NFTs based on price, nftsToAdd: ', - nftsToAdd, - ); - nftsToAdd.forEach((nft) => { const cartItem = { nft, collection, quantity: 1, salePrice: 0 }; this.cartService.addToCart(cartItem); @@ -268,7 +232,6 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { } public ngOnDestroy(): void { - console.log('ngOnDestroy fires'); this.destroy$.next(); this.destroy$.complete(); } diff --git a/src/app/pages/nft/pages/nft/nft.page.html b/src/app/pages/nft/pages/nft/nft.page.html index 247ddc2..c003dd7 100644 --- a/src/app/pages/nft/pages/nft/nft.page.html +++ b/src/app/pages/nft/pages/nft/nft.page.html @@ -275,17 +275,41 @@

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

- + + + + + + + + + + Date: Mon, 5 Feb 2024 19:33:56 -0500 Subject: [PATCH 08/24] backed out minor config file change --- tailwind.config.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tailwind.config.js b/tailwind.config.js index 09a4f53..873964d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -121,12 +121,6 @@ module.exports = { }, flex: { 2: '2 2 0%', - }, - marginLeft: { - 'sm1': '0.1em', - 'sm25': '0.25em', - 'sm50': '0.5em', - 'sm75': '0.75em', } }, }, From 30fd69d8e45f29a0460d18b7258e01589db20799 Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Mon, 5 Feb 2024 20:04:17 -0500 Subject: [PATCH 09/24] minor config styling issue fix --- tailwind.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailwind.config.js b/tailwind.config.js index 873964d..6319b01 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -121,7 +121,7 @@ module.exports = { }, flex: { 2: '2 2 0%', - } + }, }, }, plugins: [require('@tailwindcss/line-clamp')], From cdefd66b6cefb74297329712af90a622d718cc3c Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Sat, 10 Feb 2024 23:31:36 -0500 Subject: [PATCH 10/24] Shopping cart revision: reactive data, real-time cross tab updates, mobile UI modifications --- .../@shell/ui/header/header.component.html | 1 + src/app/@shell/ui/header/header.component.ts | 4 +- .../mobile-header.component.html | 19 + .../mobile-header/mobile-header.component.ts | 7 + .../cart-modal/cart-modal.component.html | 514 +++++++++++------- .../cart-modal/cart-modal.component.ts | 170 ++---- .../checkout/checkout-overlay.component.html | 309 +++++++---- .../checkout/checkout-overlay.component.ts | 101 ++-- .../components/cart/services/cart.service.ts | 457 ++++++++++------ .../nft-card/nft-card.component.html | 26 +- .../components/nft-card/nft-card.component.ts | 2 +- .../collection/nfts/collectionNfts.service.ts | 34 +- .../pages/collection/nfts/nfts.page.ts | 94 ++-- src/app/pages/nft/pages/nft/nft.page.html | 33 +- src/app/pages/nft/pages/nft/nft.page.ts | 9 +- 15 files changed, 1055 insertions(+), 725 deletions(-) diff --git a/src/app/@shell/ui/header/header.component.html b/src/app/@shell/ui/header/header.component.html index 4a5bb19..32918d6 100644 --- a/src/app/@shell/ui/header/header.component.html +++ b/src/app/@shell/ui/header/header.component.html @@ -18,6 +18,7 @@ [getNotificationDetails]="getNotificationDetails" (wenOnVisibleChange)="isMobileMenuVisible = !isMobileMenuVisible" (wenOnNotificationVisibleChange)="notificationVisibleChange()" + [cartItemCount]="cartItemCount" > diff --git a/src/app/@shell/ui/header/header.component.ts b/src/app/@shell/ui/header/header.component.ts index 66254c2..bb8da6e 100644 --- a/src/app/@shell/ui/header/header.component.ts +++ b/src/app/@shell/ui/header/header.component.ts @@ -113,7 +113,7 @@ export class HeaderComponent implements OnInit, OnDestroy { ) {} public ngOnInit(): void { - this.member$.pipe(untilDestroyed(this)).subscribe((obj) => { + this.member$.pipe(untilDestroyed(this)).subscribe((obj) => { if (obj?.uid) { this.cancelAccessSubscriptions(); this.accessSubscriptions$.push( @@ -259,7 +259,7 @@ export class HeaderComponent implements OnInit, OnDestroy { switch (checkoutTransaction.source) { case 'cartCheckout': { if (!this.cartService.isCheckoutOverlayOpen()) { - this.cartService.openCartAndCheckoutOverlay(); + this.cartService.openCheckoutOverlay(); this.cd.markForCheck(); } break; diff --git a/src/app/@shell/ui/mobile-header/mobile-header.component.html b/src/app/@shell/ui/mobile-header/mobile-header.component.html index 650eeca..7888260 100644 --- a/src/app/@shell/ui/mobile-header/mobile-header.component.html +++ b/src/app/@shell/ui/mobile-header/mobile-header.component.html @@ -46,6 +46,25 @@ >
+ + +
NotificationContent; @Output() wenOnVisibleChange = new EventEmitter(); @Output() wenOnNotificationVisibleChange = new EventEmitter(); @@ -36,6 +38,7 @@ export class MobileHeaderComponent { public location: Location, public routerService: RouterService, public unitsService: UnitsService, + public cartService: CartService, ) {} public setMobileMenuVisible(isVisible: boolean): void { @@ -56,4 +59,8 @@ export class MobileHeaderComponent { public get isLoggedIn$(): BehaviorSubject { return this.auth.isLoggedIn$; } + + public handleOpenCartModal(): void { + this.cartService.showCartModal(); + } } diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.html b/src/app/components/cart/components/cart-modal/cart-modal.component.html index 501e62e..e22c621 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.html +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.html @@ -4,223 +4,349 @@ nzClosable="true" (nzOnCancel)="handleClose()" [nzTitle]="modalHeader" - [nzWidth]="'75%'" + [nzWidth]="'80%'" >
- +
-
-
- -
-
-
-
Info
-
-
- - -
-
Status
+ +
+ +
+ {{ item.nft.name }} +
- -
-
- Qty Added / Available -
-
+
+
- -
-
- Price Each (USD Value) -
-
+ +
+
+ NFT +
+
+ {{ item.nft.name }} +
+
- -
-
Remove
-
-
-
- -
-
-
- {{ item.nft.name }} -
- +
- Collection: - +
+ Collection +
+
{{ item.collection.name }} - +
+ +
- NFT: - - {{ item.nft.name }} - +
+ Royalties +
+
+ {{ (item.collection.royaltiesFee || 0) * 100 }}% +
+
+ + +
+ +
+ Status +
+ + {{ cartStatus.status }} + + + + {{ cartStatus.status }} + + +
+
+ + +
+
+ Qty Added / Available +
+ + + + + + / {{ (cartService.getAvailableNftQuantity(item) | async) }} + + + + +
+ + +
+
+ Price Each (USD Value) +
+ +
+ {{ + item.pricing.originalPrice + | formatToken + : item.pricing.tokenSymbol + : true + : true + | async + }} +   ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ value: item.pricing.originalPrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), + item.pricing.tokenSymbol + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }} + USD) +
+
+ {{ + item.pricing.discountedPrice + | formatToken + : item.pricing.tokenSymbol + : true + : true + | async + }} +   ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ value: item.pricing.discountedPrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), + item.pricing.tokenSymbol + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }} + USD) +
+
+ +
-
+
-
Royalties: {{ (item.collection.royaltiesFee || 0) * 100 }}%
+
+ -
- - - {{ cartItemStatus(item).status }} - - - - {{ cartItemStatus(item).status }} - - -
+ +
+
+
+
+
+
Info
+
+
-
- -
- - - - - - / {{ cartItemsQuantities[i] }} - - - - +
+
Status
-
-
- - -
- {{ - cartItemPrices[item.nft.uid].originalPrice - | formatToken - : (item.nft.placeholderNft - ? item.collection.mintingData?.network - : item.nft.mintingData?.network) - : true - : true - | async - }} -   ({{ - unitsService.getUsd( - cartItemPrices[item.nft.uid].originalPrice, - cartItemPrices[item.nft.uid].tokenSymbol - ) - | async - | currency : 'USD' - | UsdBelowTwoDecimals - }} - USD) +
+
+ Qty Added / Available
-
- {{ - cartItemPrices[item.nft.uid].discountedPrice - | formatToken - : (item.nft.placeholderNft - ? item.collection.mintingData?.network - : item.nft.mintingData?.network) - : true - : true - | async - }} -   ({{ - unitsService.getUsd( - cartItemPrices[item.nft.uid].discountedPrice, - cartItemPrices[item.nft.uid].tokenSymbol - ) - | async - | currency : 'USD' - | UsdBelowTwoDecimals - }} - USD) +
+ +
+
+ Price Each (USD Value)
- - -
-
-
+
+ +
+
Remove
+
+
-
- - +
+
+
+ {{ item.nft.name }} +
+
+ Collection: + + {{ item.collection.name }} + +
+
+ NFT: + + {{ item.nft.name }} + +
+
Royalties: {{ (item.collection.royaltiesFee || 0) * 100 }}%
+
+
+ +
+ + + {{ cartStatus.status }} + + + + {{ cartStatus.status }} + + + +
+ +
+
+ + + + + + / {{ (cartService.getAvailableNftQuantity(item) | async) }} + + + + +
+
+ +
+ +
+ {{ + item.pricing.originalPrice + | formatToken + : item.pricing.tokenSymbol + : true + : true + | async + }} +   ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ value: item.pricing.originalPrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), + item.pricing.tokenSymbol + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }} + USD) +
+
+ {{ + item.pricing.discountedPrice + | formatToken + : item.pricing.tokenSymbol + : true + : true + | async + }} +   ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ value: item.pricing.discountedPrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), + item.pricing.tokenSymbol + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }} + USD) +
+
+ +
-
+
+
+ +
+ +
-
+ +
Your cart is empty. @@ -236,6 +362,20 @@ > Close + + +
- + +
@@ -49,106 +50,213 @@

- - - - NFT Name - Collection Name - Royalties Fee - Quantity Added - - Price ({{ group.tokenSymbol }}) (USD Value) - - Line Item Total (USD Value) - - - - + + + + + NFT Info + Qty + Price + + + + + + + +
+ + NFT: + + + {{ item.nft.name }} + +
+ + +
+ + COL: + + + {{ item.collection.name }} + +
+ + +
+ + Royalties: + + + {{ (item.collection.royaltiesFee || 0) * 100 }}% + +
+ {{ item.quantity }} + + {{ + item.salePrice + | formatToken + : (item.nft.placeholderNft + ? item.collection.mintingData?.network + : item.nft.mintingData?.network) + : true + : true + | async + }} +
+ ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ value: item.salePrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), + group.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }} + USD) + + +
- - {{ item.nft.name }} - + + Total + - - {{ item.collection.name }} - + + {{ group.totalQuantity }} + - {{ (item.collection.royaltiesFee || 0) * 100 }}% - {{ item.quantity }} - {{ - item.salePrice - | formatToken - : (item.nft.placeholderNft - ? item.collection.mintingData?.network - : item.nft.mintingData?.network) - : true - : true - | async - }} -   ({{ - unitsService.getUsd(item.salePrice, group.network) - | async - | currency : 'USD' - | UsdBelowTwoDecimals - }} - USD) + + {{ group.totalPrice | formatToken : group.network : true : true | async }} + + + + +
+
+ + + + + + NFT Name + Collection Name + Royalties Fee + Quantity Added + + Price ({{ group.tokenSymbol }}) (USD Value) + + Line Item Total (USD Value) + + + + + + + + {{ item.nft.name }} + + + + + {{ item.collection.name }} + + + {{ (item.collection.royaltiesFee || 0) * 100 }}% + {{ item.quantity }} + + {{ + item.salePrice + | formatToken + : (item.nft.placeholderNft + ? item.collection.mintingData?.network + : item.nft.mintingData?.network) + : true + : true + | async + }} +   ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ value: item.salePrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), + group.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }} + USD) + + + {{ + item.quantity * item.salePrice + | formatToken + : (item.nft.placeholderNft + ? item.collection.mintingData?.network + : item.nft.mintingData?.network) + : true + : true + | async + }} +   ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ value: (item.quantity * item.salePrice) || 0, exponents: cartService.getDefaultNetworkDecimals() }), + group.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }} + USD) + + + + + + + Total + + {{ group.totalQuantity }} + - {{ - item.quantity * item.salePrice - | formatToken - : (item.nft.placeholderNft - ? item.collection.mintingData?.network - : item.nft.mintingData?.network) - : true - : true - | async - }} -   ({{ - unitsService.getUsd(item.quantity * item.salePrice, group.network) - | async - | currency : 'USD' - | UsdBelowTwoDecimals - }} - USD) + + {{ group.totalPrice | formatToken : group.network : true : true | async }} + -
- - - - Total - - {{ group.totalQuantity }} - - - - - {{ group.totalPrice | formatToken : group.network : true : true | async }} - - - - -
+ + + +

@@ -252,7 +360,7 @@

class="block w-full" *ngIf="!helper.isExpired(transaction$ | async)" [targetAddress]="targetAddress" - [formattedAmount]="targetAmount | formatToken : mintingDataNetwork : true | async" + [formattedAmount]="targetAmount | formatToken : getSelectedNetwork() : true | async" > @@ -260,7 +368,7 @@

*ngIf="!helper.isExpired(transaction$ | async)" class="mt-6" [targetAddress]="targetAddress" - [network]="mintingDataNetwork" + [network]="getSelectedNetwork()" [targetAmount]="(targetAmount || 0).toString()" > @@ -346,5 +454,18 @@

Close checkout

+ +
+ +
diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.ts b/src/app/components/cart/components/checkout/checkout-overlay.component.ts index ded5948..678663e 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.ts +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.ts @@ -24,7 +24,8 @@ import { AuthService } from '@components/auth/services/auth.service'; import { NotificationService } from '@core/services/notification'; import { OrderApi } from '@api/order.api'; import { NftApi } from '@api/nft.api'; -import { BehaviorSubject, firstValueFrom, interval, Subscription } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, interval, Observable, Subscription, forkJoin, of } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; import { TransactionService } from '@core/services/transaction'; import { removeItem, @@ -40,6 +41,7 @@ import { Router } from '@angular/router'; import { NzNotificationService } from 'ng-zorro-antd/notification'; import { ThemeList, ThemeService } from '@core/services/theme'; import { UnitsService } from '@core/services/units/units.service'; +import { DeviceService } from '@core/services/device'; export enum StepType { CONFIRM = 'Confirm', @@ -81,7 +83,6 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { public groupedCartItems: GroupedCartItem[] = []; public unavailableItemCount = 0; - public cartItemPrices: { [key: string]: { originalPrice: number; discountedPrice: number } } = {}; public agreeTermsConditions = false; public transaction$: BehaviorSubject = new BehaviorSubject< Transaction | undefined @@ -101,11 +102,11 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { private purchasedTokenSymbol: string | null = null; private transSubscription$?: Subscription; public nftPath = ROUTER_UTILS.config.nft.root; - public collectionPath: string = ROUTER_UTILS.config.collection.root; + public collectionPath = ROUTER_UTILS.config.collection.root; public expandedGroups = new Set(); private currentTransactionSubscription?: Subscription; - public theme = ThemeList; + constructor( public cartService: CartService, private auth: AuthService, @@ -119,6 +120,7 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { private nzNotification: NzNotificationService, public themeService: ThemeService, public unitsService: UnitsService, + public deviceService: DeviceService, ) {} public get themes(): typeof ThemeList { @@ -376,7 +378,8 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { this.cd.markForCheck(); } - public toggleGroup(groupSymbol: string) { + public toggleGroup(event: MouseEvent, groupSymbol: string): void { + event.stopPropagation(); if (this.expandedGroups.has(groupSymbol)) { this.expandedGroups.delete(groupSymbol); } else { @@ -403,42 +406,46 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { } public groupItems() { - const groups: { [tokenSymbol: string]: GroupedCartItem } = {}; - this.items.forEach((item) => { - const tokenSymbol = - (item.nft?.placeholderNft - ? item.collection?.mintingData?.network - : item.nft?.mintingData?.network) || DEFAULT_NETWORK; - const discount = this.discount(item); - const originalPrice = this.calcPrice(item, 1); - const discountedPrice = this.calcPrice(item, discount); - const price = this.discount(item) < 1 ? discountedPrice : originalPrice; - item.salePrice = price; - - const network = - (item.nft?.placeholderNft - ? item.collection?.mintingData?.network - : item.nft?.mintingData?.network) || DEFAULT_NETWORK; - - if (this.cartService.isCartItemAvailableForSale(item).isAvailable) { + const availabilityChecks$ = this.items.map(item => + this.cartService.isCartItemAvailableForSale(item).pipe( + map(result => ({ item, isAvailable: result.isAvailable })), + switchMap(result => result ? of(result) : of({ item, isAvailable: false })) + ) + ); + + forkJoin(availabilityChecks$).subscribe(results => { + const groups: { [tokenSymbol: string]: GroupedCartItem } = {}; + this.unavailableItemCount = 0; + + results.forEach(({ item, isAvailable }) => { + if (!isAvailable) { + this.unavailableItemCount++; + return; + } + + const tokenSymbol = + (item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network) || DEFAULT_NETWORK; + const discount = this.discount(item); + const originalPrice = this.calcPrice(item, 1); + const discountedPrice = this.calcPrice(item, discount); + const price = discount < 1 ? discountedPrice : originalPrice; + item.salePrice = price; + + const network = + (item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network) || DEFAULT_NETWORK; + if (!groups[tokenSymbol]) { - groups[tokenSymbol] = { - tokenSymbol, - items: [], - totalQuantity: 0, - totalPrice: 0, - network, - }; + groups[tokenSymbol] = { tokenSymbol, items: [], totalQuantity: 0, totalPrice: 0, network }; } + groups[tokenSymbol].items.push(item); groups[tokenSymbol].totalQuantity += item.quantity; groups[tokenSymbol].totalPrice += item.quantity * item.salePrice; - } else { - this.unavailableItemCount++; - } - }); + }); - this.groupedCartItems = Object.values(groups); + this.groupedCartItems = Object.values(groups); + this.cd.markForCheck(); + }); } public updateStep(step: StepType) { @@ -453,17 +460,18 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { } private calcPrice(item: CartItem, discount: number): number { - return this.cartService.calcPrice(item, discount); + return this.cartService.calcPrice(item.nft, discount); } private discount(item: CartItem): number { return this.cartService.discount(item.collection, item.nft); } - public isCartItemAvailableForSale(item: CartItem): any { - return this.cartService.isCartItemAvailableForSale(item).isAvailable; + public isCartItemAvailableForSale(item: CartItem): Observable { + return this.cartService.isCartItemAvailableForSale(item).pipe(map(result => result.isAvailable)); } + public reset(): void { this.receivedTransactions = false; this.currentStep = StepType.CONFIRM; @@ -497,6 +505,18 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { this.handleClose(true); } + public goToMemberNfts(): void { + + const memberId = this.auth.member$.value?.uid; + + if(!memberId) { + return; + } + + this.router.navigate(['/member', memberId, 'nfts']); + this.handleClose(true); + } + public getRecords(): Nft[] | null | undefined { return this.purchasedNfts || null; } @@ -653,6 +673,11 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { }); } + public getSelectedNetwork(): any { + const selectedNetwork = localStorage.getItem('cartCheckoutSelectedNetwork') || ''; + return selectedNetwork; + } + ngOnDestroy() { this.currentTransactionSubscription?.unsubscribe(); } diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts index 6991538..4e4cc7c 100644 --- a/src/app/components/cart/services/cart.service.ts +++ b/src/app/components/cart/services/cart.service.ts @@ -1,5 +1,5 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable, Subscription, map } from 'rxjs'; +import { Injectable, NgZone } from '@angular/core'; +import { BehaviorSubject, Observable, Subscription, map, of, take, tap, catchError, finalize, combineLatest } from 'rxjs'; import { Nft, Collection, @@ -7,27 +7,39 @@ import { MIN_AMOUNT_TO_TRANSFER, TRANSACTION_AUTO_EXPIRY_MS, DEFAULT_NETWORK, - Space, - Award, + getDefDecimalIfNotSet, CollectionStatus, + DEFAULT_NETWORK_DECIMALS, + Network, } from '@build-5/interfaces'; -import { getItem, setItem, removeItem, StorageItem, getCheckoutTransaction } from '@core/utils'; +import { getItem, removeItem, StorageItem, getCheckoutTransaction } from '@core/utils'; import { NzNotificationService } from 'ng-zorro-antd/notification'; import { HelperService } from '@pages/nft/services/helper.service'; import { AuthService } from '@components/auth/services/auth.service'; import { NzModalRef, NzModalService } from 'ng-zorro-antd/modal'; -import { CheckoutOverlayComponent } from '../components/checkout/checkout-overlay.component'; +import { CheckoutOverlayComponent } from '@components/cart/components/checkout/checkout-overlay.component'; import { OrderApi } from '@api/order.api'; import { SpaceApi } from '@api/space.api'; import { MemberApi } from '@api/member.api'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import dayjs from 'dayjs'; +import { NftApi } from '@api/nft.api'; export interface CartItem { nft: Nft; collection: Collection; quantity: number; salePrice: number; + pricing: { + originalPrice: number; + discountedPrice: number; + tokenSymbol: Network; + }; +} + +export interface ConvertValue { + value: number | null | undefined; + exponents: number | null | undefined; } export enum StepType { @@ -37,6 +49,8 @@ export enum StepType { COMPLETE = 'Complete', } +export const CART_STORAGE_KEY = 'App/cartItems'; + @Injectable({ providedIn: 'root', }) @@ -53,10 +67,12 @@ export class CartService { private pendingTransaction$: BehaviorSubject = new BehaviorSubject< Transaction | undefined >(undefined); - private memberSpaces: string[] = []; - private memberGuardianSpaces: string[] = []; - private memberAwards: string[] = []; + private memberSpacesSubject$ = new BehaviorSubject([]); + private memberGuardianSpacesSubject$ = new BehaviorSubject([]); + private memberAwardsSubject$ = new BehaviorSubject([]); private guardianSpaceSubscriptions$: { [key: string]: Subscription } = {}; + private isLoadingSubject$ = new BehaviorSubject(false); + public isLoading$ = this.isLoadingSubject$.asObservable(); constructor( private notification: NzNotificationService, @@ -66,8 +82,22 @@ export class CartService { private orderApi: OrderApi, private spaceApi: SpaceApi, private memberApi: MemberApi, + private zone: NgZone, + private nftApi: NftApi, ) { this.subscribeToMemberChanges(); + this.listenToStorageChanges(); + } + + private listenToStorageChanges(): void { + window.addEventListener('storage', (event) => { + if (event.storageArea === localStorage && event.key === CART_STORAGE_KEY) { + this.zone.run(() => { + const updatedCartItems = JSON.parse(event.newValue || '[]'); + this.cartItemsSubject$.next(updatedCartItems); + }); + } + }); } private subscribeToMemberChanges() { @@ -90,44 +120,29 @@ export class CartService { } private resetMemberData() { - this.memberSpaces = []; - this.memberGuardianSpaces = []; - this.memberAwards = []; + this.memberSpacesSubject$.next([]); + this.memberGuardianSpacesSubject$.next([]); + this.memberAwardsSubject$.next([]); this.cleanupGuardianSubscriptions(); } private loadMemberSpaces(memberId: string): void { - this.memberApi - .allSpacesAsMember(memberId) - .pipe(untilDestroyed(this)) - .subscribe((spaces) => { - if (spaces) { - this.memberSpaces = spaces.map((space) => space.uid); - this.updateMemberSpaces(this.memberSpaces); - } - }); + this.memberApi.allSpacesAsMember(memberId).pipe( + untilDestroyed(this), + map(spaces => spaces.map(space => space.uid)), + ).subscribe(spaceIds => { + this.memberSpacesSubject$.next(spaceIds); + spaceIds.forEach(spaceId => this.updateMemberGuardianStatus(spaceId)); + }); } private loadMemberAwards(memberId: string): void { - this.memberApi - .topAwardsCompleted(memberId) - .pipe(untilDestroyed(this)) - .subscribe((awards) => { - if (awards) { - this.memberAwards = awards.map((award) => award.uid); - this.updateMemberAwards(this.memberAwards); - } - }); - } - - private updateMemberSpaces(spaces: string[]) { - this.memberSpaces = spaces.map((space) => space); - this.memberGuardianSpaces = []; - spaces.forEach((space) => this.updateMemberGuardianStatus(space)); - } - - private updateMemberAwards(awards: string[]) { - this.memberAwards = awards.map((award) => award); + this.memberApi.topAwardsCompleted(memberId).pipe( + untilDestroyed(this), + map(awards => awards.map(award => award.uid)), + ).subscribe(awardIds => { + this.memberAwardsSubject$.next(awardIds); + }); } private updateMemberGuardianStatus(spaceId: string) { @@ -137,15 +152,21 @@ export class CartService { .isGuardianWithinSpace(spaceId, this.auth.member$.value?.uid) .pipe(untilDestroyed(this)) .subscribe((isGuardian) => { - if (isGuardian && !this.memberGuardianSpaces.includes(spaceId)) { - this.memberGuardianSpaces.push(spaceId); - } else if (!isGuardian && this.memberGuardianSpaces.includes(spaceId)) { - this.memberGuardianSpaces = this.memberGuardianSpaces.filter((id) => id !== spaceId); - } + this.updateGuardianSpaces(isGuardian, spaceId); }); } } + private updateGuardianSpaces(isGuardian: boolean, spaceId: string) { + let currentGuardianSpaces = this.memberGuardianSpacesSubject$.getValue(); + if (isGuardian && !currentGuardianSpaces.includes(spaceId)) { + currentGuardianSpaces.push(spaceId); + } else if (!isGuardian && currentGuardianSpaces.includes(spaceId)) { + currentGuardianSpaces = currentGuardianSpaces.filter((id) => id !== spaceId); + } + this.memberGuardianSpacesSubject$.next(currentGuardianSpaces); + } + private cleanupGuardianSubscriptions() { Object.values(this.guardianSpaceSubscriptions$).forEach((subscription) => subscription.unsubscribe(), @@ -153,6 +174,18 @@ export class CartService { this.guardianSpaceSubscriptions$ = {}; } + get memberSpaces$(): Observable { + return this.memberSpacesSubject$.asObservable(); + } + + get memberGuardianSpaces$(): Observable { + return this.memberGuardianSpacesSubject$.asObservable(); + } + + get memberAwards$(): Observable { + return this.memberAwardsSubject$.asObservable(); + } + public hasPendingTransaction(): boolean { const checkoutTransaction = getCheckoutTransaction(); const transactionId = checkoutTransaction?.transactionId; @@ -185,7 +218,30 @@ export class CartService { } public showCartModal(): void { - this.cartModalOpenSubject$.next(true); + this.isLoadingSubject$.next(true); + + const cartItems = this.cartItemsSubject$.getValue(); + if (cartItems.length === 0) { + this.isLoadingSubject$.next(false); + this.cartModalOpenSubject$.next(true); + return; + } + + const freshDataObservables = cartItems.map(item => + this.nftApi.getNftById(item.nft.uid).pipe( + take(1), + map(freshNft => freshNft ? { ...item, nft: freshNft } : item), + catchError(() => of(item)) + ) + ); + + combineLatest(freshDataObservables).pipe( + take(1), + finalize(() => this.isLoadingSubject$.next(false)) + ).subscribe(updatedCartItems => { + this.cartItemsSubject$.next(updatedCartItems); + this.cartModalOpenSubject$.next(true); + }); } public hideCartModal(): void { @@ -197,65 +253,67 @@ export class CartService { } public openCheckoutOverlay(): void { + if (!this.checkoutOverlayOpenSubject$.getValue()) { + const checkoutTransaction = getCheckoutTransaction(); if (checkoutTransaction && checkoutTransaction.transactionId) { if (checkoutTransaction.source === 'nftCheckout') { - this.notification.error( - 'You currently have an open order. Pay for it or let it expire.', - '', - ); + this.notification.error('You currently have an open order. Pay for it or let it expire.', ''); return; } if (checkoutTransaction.source === 'cartCheckout') { this.setCurrentTransaction(checkoutTransaction.transactionId); - const currentTransaction = this.pendingTransaction$.getValue(); - - if (currentTransaction && currentTransaction.uid) { - const expiresOn: dayjs.Dayjs = dayjs(currentTransaction.createdOn!.toDate()).add( - TRANSACTION_AUTO_EXPIRY_MS, - 'ms', - ); - - if ( - expiresOn.isBefore(dayjs()) || - currentTransaction.payload?.void || - currentTransaction.payload?.reconciled - ) { - removeItem(StorageItem.CheckoutTransaction); - this.pendingTransaction$.next(undefined); - this.setCurrentStep(StepType.CONFIRM); + this.getCurrentTransaction().pipe(take(1)).subscribe(currentTransaction => { + if (currentTransaction && currentTransaction.uid) { + const expiresOn: dayjs.Dayjs = dayjs(currentTransaction.createdOn!.toDate()).add(TRANSACTION_AUTO_EXPIRY_MS, 'ms'); + + if (expiresOn.isBefore(dayjs()) || currentTransaction.payload?.void || currentTransaction.payload?.reconciled) { + removeItem(StorageItem.CheckoutTransaction); + this.pendingTransaction$.next(undefined); + this.setCurrentStep(StepType.CONFIRM); + this.openModal(); + } else { + this.openModal(); + } + } else { + this.openModal(); } - } + }); } else { - this.notification.error( - 'CheckoutTransaction exists and source is not nftCheckout, the checkout-overlay is not open and the pending transaction not expired or complete and the source is not cartCheckout, this should never happen.', - '', - ); + this.notification.error('CheckoutTransaction exists and source is not nftCheckout, the checkout-overlay is not open and the pending transaction not expired or complete and the source is not cartCheckout, this should never happen.', ''); return; } + } else { + this.openModal(); } + } + } - const cartItems = this.getCartItems().getValue(); - - this.checkoutOverlayModalRef = this.modalService.create({ - nzTitle: 'Checkout', - nzContent: CheckoutOverlayComponent, - nzComponentParams: { items: cartItems }, - nzFooter: null, - nzWidth: '80%', - }); + private openModal(): void { + this.getCartItems().pipe(take(1)).subscribe(cartItems => { + if (cartItems.length > 0) { + this.checkoutOverlayModalRef = this.modalService.create({ + nzTitle: 'Checkout', + nzContent: CheckoutOverlayComponent, + nzComponentParams: { items: cartItems }, + nzFooter: null, + nzWidth: '80%', + }); - this.checkoutOverlayModalRef.afterClose.subscribe(() => { - this.closeCheckoutOverlay(); - }); + this.checkoutOverlayModalRef.afterClose.subscribe(() => { + this.closeCheckoutOverlay(); + }); - this.checkoutOverlayOpenSubject$.next(true); - } + this.checkoutOverlayOpenSubject$.next(true); + } + }); } + + public closeCheckoutOverlay(): void { const checkoutTransaction = getCheckoutTransaction(); if (checkoutTransaction && checkoutTransaction.transactionId) { @@ -340,32 +398,83 @@ export class CartService { } } - public getCartItems(): BehaviorSubject { - return this.cartItemsSubject$; + public getCartItems(): Observable { + return this.cartItemsSubject$.asObservable(); } - public addToCart(cartItem: CartItem): void { - const currentItems = this.cartItemsSubject$.value; - if (currentItems.length >= 100) { - this.notification.error( - $localize`Your cart is full. Please remove items before adding more.`, - '', + public cartItemStatus(item: CartItem): Observable<{ status: string; message: string }> { + return this.isCartItemAvailableForSale(item) + .pipe( + map(availabilityResult => ({ + status: availabilityResult.isAvailable ? 'Available' : 'Not Available', + message: availabilityResult.message + })) ); + } + + public addToCart(nft: Nft, collection: Collection, quantity: number = 1): void { + const currentItems = this.cartItemsSubject$.getValue(); + const existingItemIndex = currentItems.findIndex(item => item.nft.uid === nft.uid); + + if (existingItemIndex > -1) { + this.notification.error('This NFT already exists in your cart.', ''); return; } - const isItemAlreadyInCart = currentItems.some((item) => item.nft.uid === cartItem.nft.uid); - if (!isItemAlreadyInCart) { - const updatedCartItems = [...currentItems, cartItem]; - this.cartItemsSubject$.next(updatedCartItems); - this.saveCartItems(); - this.notification.success( - $localize`NFT ${cartItem.nft.name} from collection ${cartItem.collection.name} has been added to your cart.`, - '', - ); - } else { - this.notification.error($localize`This NFT already exists in your cart.`, ''); + const discountRate = this.discount(collection, nft); + const originalPrice = this.calcPrice(nft, 1); + const discountedPrice = this.calcPrice(nft, discountRate); + const tokenSymbol = (nft.placeholderNft ? collection.mintingData?.network : nft.mintingData?.network) || DEFAULT_NETWORK; + + const cartItem: CartItem = { + nft: nft, + collection: collection, + quantity: quantity, + salePrice: discountRate < 1 ? discountedPrice : originalPrice, + pricing: { + originalPrice, + discountedPrice, + tokenSymbol, + }, + }; + + const updatedCartItems = [...currentItems, cartItem]; + this.cartItemsSubject$.next(updatedCartItems); + this.saveCartItems(); + + this.notification.success(`NFT ${nft.name} from collection ${collection.name} has been added to your cart.`, ''); + } + + + public discount(collection?: Collection | null, nft?: Nft | null): number { + if (!collection?.space || !this.auth.member$.value || nft?.owner) { + return 1; + } + + const spaceRewards = (this.auth.member$.value.spaces || {})[collection.space]; + const descDiscounts = [...(collection.discounts || [])].sort((a, b) => b.amount - a.amount); + for (const discount of descDiscounts) { + const awardStat = (spaceRewards?.awardStat || {})[discount.tokenUid!]; + const memberTotalReward = awardStat?.totalReward || 0; + if (memberTotalReward >= discount.tokenReward) { + return 1 - discount.amount; + } + } + return 1; + } + + public calc(amount: number | null | undefined, discount: number): number { + let finalPrice = Math.ceil((amount || 0) * discount); + if (finalPrice < MIN_AMOUNT_TO_TRANSFER) { + finalPrice = MIN_AMOUNT_TO_TRANSFER; } + + return finalPrice; + } + + public calcPrice(nft: Nft, discount: number): number { + const itemPrice = nft?.availablePrice || nft?.price || 0; + return this.calc(itemPrice, discount); } public refreshCartItems(): void { @@ -404,23 +513,67 @@ export class CartService { this.saveCartItems(); } + public updateCartItemQuantity(itemId: string, newQuantity: number): void { + this.cartItemsSubject$.pipe( + take(1), + tap(cartItems => { + const updatedCartItems = cartItems.map(item => { + if (item.nft.uid === itemId) { + return { ...item, quantity: newQuantity }; + } + return item; + }); + + this.cartItemsSubject$.next(updatedCartItems); + + this.saveCartItems(); + }) + ).subscribe(); + } + + public getSelectedNetwork(): any { + return localStorage.getItem('cartCheckoutSelectedNetwork') || ''; + } + public isNftAvailableForSale( nft: Nft, collection: Collection, - ): { isAvailable: boolean; message: string } { + checkCartPresence: boolean = false + ): Observable<{ isAvailable: boolean; message: string }> { let message = 'NFT is available for sale.'; const conditions: string[] = []; let isAvailable = false; + const memberSpaces = this.memberSpacesSubject$.getValue(); + const memberGuardianSpaces = this.memberGuardianSpacesSubject$.getValue(); + const memberAwards = this.memberAwardsSubject$.getValue(); + + if (checkCartPresence) { + const isNftInCart = this.cartItemsSubject$.getValue().some(cartItem => cartItem.nft.uid === nft.uid); + if (isNftInCart) { + message = 'This NFT is already in your cart.'; + return of({ + isAvailable, + message + }); + } + } + if (!collection) { message = 'Internal Error: Collection data is null or undefined.'; - return { isAvailable, message }; + return of({ + isAvailable, + message + }); } if (!nft?.availableFrom) { message = 'Internal Error: Nft and/or NFT Available From date is null or undefined.'; - return { isAvailable, message }; + return of({ + isAvailable, + message + }); } let validAvailableFromDate = @@ -454,23 +607,23 @@ export class CartService { if (isOwner) conditions.push('You are the owner of this NFT.'); const availableValue = +nft?.available; - const nftAvailable = availableValue === 1 || availableValue === 3; + const nftAvailable = availableValue === 1 || availableValue === 3 || nft?.available === null || nft?.available === undefined; if (!nftAvailable) conditions.push('NFT is not marked as available.'); const spaceMemberAccess = collection?.access !== 1 || - (collection?.access === 1 && this.memberSpaces.includes(collection?.space ?? '')); + (collection?.access === 1 && memberSpaces.includes(collection?.space ?? '')); if (!spaceMemberAccess) conditions.push('Member does not have access to this space.'); const spaceGuardianAccess = collection?.access !== 2 || - (collection?.access === 2 && this.memberGuardianSpaces.includes(collection?.space ?? '')); + (collection?.access === 2 && memberGuardianSpaces.includes(collection?.space ?? '')); if (!spaceGuardianAccess) conditions.push('Member is not a guardian of this space.'); const spaceAwardAccess = collection?.access !== 3 || (collection?.access === 3 && - collection?.accessAwards?.some((award) => this.memberAwards.includes(award))); + collection?.accessAwards?.some((award) => memberAwards.includes(award))); if (!spaceAwardAccess) conditions.push('Member does not have the required awards for access.'); isAvailable = @@ -490,7 +643,22 @@ export class CartService { 'NFT is not available for sale due to the following conditions: ' + conditions.join(' '); } - return { isAvailable, message }; + return of({ + isAvailable, + message + }); + } + + public isCartItemAvailableForSale(cartItem: CartItem, checkCartPresence: boolean = false): Observable<{ isAvailable: boolean; message: string }> { + return this.isNftAvailableForSale(cartItem.nft, cartItem.collection, checkCartPresence) + .pipe( + map(result => { + return { + isAvailable: result.isAvailable, + message: result.message + }; + }) + ); } public clearCart(): void { @@ -499,33 +667,22 @@ export class CartService { this.notification.success($localize`All items have been removed from your cart.`, ''); } - public isCartItemAvailableForSale(cartItem: CartItem): { isAvailable: boolean; message: string } { - const isAvailable = this.isNftAvailableForSale(cartItem.nft, cartItem.collection).isAvailable; - const message = this.isNftAvailableForSale(cartItem.nft, cartItem.collection).message; - return { isAvailable, message }; - } - public getAvailableNftQuantity(cartItem: CartItem): number { - const isAvailableForSale = this.helperService.isAvailableForSale( - cartItem.nft, - cartItem.collection, + public getAvailableNftQuantity(cartItem: CartItem): Observable { + return this.isCartItemAvailableForSale(cartItem).pipe( + map(result => { + if (result.isAvailable) { + return cartItem.nft.placeholderNft ? (cartItem.collection.availableNfts || 0) : 1; + } else { + return 0; + } + }), + map(quantity => quantity === null ? 0 : quantity) ); - - if (cartItem.nft.placeholderNft && isAvailableForSale) { - return cartItem.collection.availableNfts || 0; - } else if (isAvailableForSale) { - return 1; - } - return 0; - } - - public updateCartItems(updatedItems: CartItem[]): void { - this.cartItemsSubject$.next(updatedItems); - this.saveCartItems(); } public saveCartItems(): void { - setItem(StorageItem.CartItems, this.cartItemsSubject$.value); + localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(this.cartItemsSubject$.getValue())); } public loadCartItems(): CartItem[] { @@ -533,34 +690,16 @@ export class CartService { return items || []; } - public discount(collection?: Collection | null, nft?: Nft | null): number { - if (!collection?.space || !this.auth.member$.value || nft?.owner) { - return 1; - } - - const spaceRewards = (this.auth.member$.value.spaces || {})[collection.space]; - const descDiscounts = [...(collection.discounts || [])].sort((a, b) => b.amount - a.amount); - for (const discount of descDiscounts) { - const awardStat = (spaceRewards?.awardStat || {})[discount.tokenUid!]; - const memberTotalReward = awardStat?.totalReward || 0; - if (memberTotalReward >= discount.tokenReward) { - return 1 - discount.amount; - } + public valueDivideExponent(value: ConvertValue): number { + if (value.exponents === 0 || value.value === null || value.value === undefined) { + return value.value!; + } else { + return value.value! / Math.pow(10, getDefDecimalIfNotSet(value.exponents)); } - return 1; } - public calc(amount: number | null | undefined, discount: number): number { - let finalPrice = Math.ceil((amount || 0) * discount); - if (finalPrice < MIN_AMOUNT_TO_TRANSFER) { - finalPrice = MIN_AMOUNT_TO_TRANSFER; - } - - return finalPrice; + public getDefaultNetworkDecimals(): number { + return DEFAULT_NETWORK_DECIMALS; } - public calcPrice(item: CartItem, discount: number): number { - const itemPrice = item.nft?.availablePrice || item.nft?.price || 0; - return this.calc(itemPrice, discount); - } } diff --git a/src/app/components/nft/components/nft-card/nft-card.component.html b/src/app/components/nft/components/nft-card/nft-card.component.html index febf5aa..7302f3b 100644 --- a/src/app/components/nft/components/nft-card/nft-card.component.html +++ b/src/app/components/nft/components/nft-card/nft-card.component.html @@ -240,39 +240,19 @@ - - + + - - - - -
diff --git a/src/app/components/nft/components/nft-card/nft-card.component.ts b/src/app/components/nft/components/nft-card/nft-card.component.ts index 4fa8cd1..5fc984f 100644 --- a/src/app/components/nft/components/nft-card/nft-card.component.ts +++ b/src/app/components/nft/components/nft-card/nft-card.component.ts @@ -194,7 +194,7 @@ export class NftCardComponent { event.preventDefault(); if (nft && collection) { - this.cartService.addToCart({ nft, collection, quantity: 1, salePrice: 0 }); + this.cartService.addToCart(nft, collection); } else { console.error('Attempted to add a null or undefined NFT or Collection to the cart'); } diff --git a/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts b/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts index e38ce92..f118c77 100644 --- a/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts +++ b/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable, combineLatest } from 'rxjs'; +import { switchMap, map, take } from 'rxjs/operators'; import { Nft, Collection } from '@build-5/interfaces'; import { CartService } from '@components/cart/services/cart.service'; @@ -14,19 +15,34 @@ export class CollectionNftStateService { constructor(private cartService: CartService) {} + public getListedNftsObservable(collection: Collection): Observable { + return this.listedNfts$.pipe( + switchMap((nfts) => + combineLatest( + nfts.map((nft) => + this.cartService.isNftAvailableForSale(nft, collection, true).pipe( + map(availability => ({ nft, isAvailable: availability.isAvailable })) + ) + ) + ) + ), + map(nftsWithAvailability => + nftsWithAvailability + .filter(({ isAvailable }) => isAvailable) + .map(({ nft }) => nft) + ) + ); + } + public setListedNfts(nfts: Nft[], collection: Collection) { this.listedNftsSubject$.next(nfts); this.updateAvailableNftsCount(nfts, collection); } private updateAvailableNftsCount(nfts: Nft[], collection: Collection) { - const availableNftsCount = nfts.filter( - (nft) => this.cartService.isNftAvailableForSale(nft, collection).isAvailable, - ).length; - this.availableNftsCountSubject$.next(availableNftsCount); - } - - public getListedNfts(): Nft[] { - return this.listedNftsSubject$.getValue(); + this.getListedNftsObservable(collection).pipe( + map(nftsForSale => nftsForSale.length), + take(1), + ).subscribe(count => this.availableNftsCountSubject$.next(count)); } } diff --git a/src/app/pages/collection/pages/collection/nfts/nfts.page.ts b/src/app/pages/collection/pages/collection/nfts/nfts.page.ts index 94b28dd..6464042 100644 --- a/src/app/pages/collection/pages/collection/nfts/nfts.page.ts +++ b/src/app/pages/collection/pages/collection/nfts/nfts.page.ts @@ -23,7 +23,7 @@ import { FilterService } from '@pages/market/services/filter.service'; import { COL, Timestamp, Collection } from '@build-5/interfaces'; import { InstantSearchConfig } from 'angular-instantsearch/instantsearch/instantsearch'; import { Subject, take, filter, takeUntil } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; +import { map, switchMap, tap } from 'rxjs/operators'; import { CartService } from '@components/cart/services/cart.service'; import { NzNotificationService } from 'ng-zorro-antd/notification'; import { state } from '@angular/animations'; @@ -109,24 +109,21 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { } private loadCollection(collectionId: string): void { - this.collectionApi - .getCollectionById(collectionId) - .pipe(take(1)) - .subscribe({ + this.collectionApi.getCollectionById(collectionId).pipe( + take(1) + ).subscribe({ next: (collectionData) => { - if (collectionData) { - this.collection = collectionData; - const listedNfts = this.collectionNftStateService.getListedNfts(); - if (this.originalNfts.length > 0 && listedNfts.length === 0) { - this.collectionNftStateService.setListedNfts(this.originalNfts, this.collection); + if (collectionData) { + this.collection = collectionData; + this.initializeAlgoliaFilters(collectionId); + } else { + this.notification.error($localize`Error occurred while fetching collection.`, ''); } - this.initializeAlgoliaFilters(collectionId); - } }, error: (err) => { - this.notification.error($localize`Error occurred while fetching collection.`, ''); + this.notification.error($localize`Error occurred while fetching collection.`, ''); }, - }); + }); } private initializeAlgoliaFilters(collectionId: string): void { @@ -185,50 +182,35 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { public sweepToCart(count: number) { if (!this.collectionId) { - this.notification.error($localize`Collection ID is not available.`, ''); - return; + this.notification.error($localize`Collection ID is not available.`, ''); + return; } - this.collectionApi - .getCollectionById(this.collectionId) - .pipe( - take(1), - filter((collection): collection is Collection => collection !== undefined), - switchMap((collection: Collection) => { - const listedNfts = this.collectionNftStateService.getListedNfts(); - - const nftsForSale = listedNfts.filter( - (nft) => this.cartService.isNftAvailableForSale(nft, collection).isAvailable, - ); - - const getEffectivePrice = (nft) => nft?.availablePrice || nft?.price || 0; - - const sortedNfts = nftsForSale.sort((a, b) => { - const priceA = getEffectivePrice(a); - const priceB = getEffectivePrice(b); - return priceA - priceB; - }); - - const nftsToAdd = sortedNfts.slice(0, Math.min(count, sortedNfts.length)); - nftsToAdd.forEach((nft) => { - const cartItem = { nft, collection, quantity: 1, salePrice: 0 }; - this.cartService.addToCart(cartItem); - }); - - this.notification.success( - $localize`NFTs swept into your cart, open cart to review added items.`, - '', - ); - - return nftsToAdd; - }), - takeUntil(this.destroy$), - ) - .subscribe({ - error: (err) => { - this.notification.error($localize`Error occurred while fetching collection.`, ''); - }, - }); + this.collectionApi.getCollectionById(this.collectionId) + .pipe( + take(1), + filter((collection): collection is Collection => Boolean(collection)), + switchMap((collection) => { + // Ensure we're working with a defined collection + if (!collection) { + throw new Error('Collection is undefined after filtering'); + } + return this.collectionNftStateService.getListedNftsObservable(collection).pipe( + map(nftsForSale => nftsForSale.slice(0, Math.min(count, nftsForSale.length))), + map(nftsToAdd => ({ nftsToAdd, collection })) + ); + }), + takeUntil(this.destroy$), + ) + .subscribe({ + next: ({ nftsToAdd, collection }) => { + nftsToAdd.forEach((nft) => { + this.cartService.addToCart(nft, collection); + }); + this.notification.success($localize`NFTs swept into your cart, open cart to review added items.`, ''); + }, + error: (error) => this.notification.error($localize`Error occurred while adding NFTs to cart.`, '') + }); } public ngOnDestroy(): void { diff --git a/src/app/pages/nft/pages/nft/nft.page.html b/src/app/pages/nft/pages/nft/nft.page.html index c003dd7..518857b 100644 --- a/src/app/pages/nft/pages/nft/nft.page.html +++ b/src/app/pages/nft/pages/nft/nft.page.html @@ -276,26 +276,23 @@

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

- - + + + + - +
-
-
- NFT -
-
+
NFT
+
{{ item.nft.name }}
-
- Collection -
-
+
Collection
+
{{ item.collection.name }}
-
- Royalties -
-
- {{ (item.collection.royaltiesFee || 0) * 100 }}% -
+
Royalties
+
{{ (item.collection.royaltiesFee || 0) * 100 }}%
- -
- Status -
+ +
Status
{{ cartStatus.status }} @@ -77,11 +98,14 @@
-
- Qty Added / Available -
+
Qty Added / Available
- + Your Cart (change)="updateQuantity($event, item.nft.uid)" min="1" [max]="(cartService.getAvailableNftQuantity(item) | async) ?? 1" - [disabled]="((cartService.getAvailableNftQuantity(item) | async) ?? 1) <= 1"> + [disabled]=" + ((cartService.getAvailableNftQuantity(item) | async) ?? 1) <= 1 + " + /> - / {{ (cartService.getAvailableNftQuantity(item) | async) }} + / {{ cartService.getAvailableNftQuantity(item) | async }} @@ -104,9 +131,7 @@
-
- Price Each (USD Value) -
+
Price Each (USD Value)
Your Cart > {{ item.pricing.originalPrice - | formatToken - : item.pricing.tokenSymbol - : true - : true + | formatToken : item.pricing.tokenSymbol : true : true | async }}   ({{ unitsService.getUsd( - cartService.valueDivideExponent({ value: item.pricing.originalPrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), + cartService.valueDivideExponent({ + value: item.pricing.originalPrice || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), item.pricing.tokenSymbol ) | async @@ -134,15 +159,15 @@
{{ item.pricing.discountedPrice - | formatToken - : item.pricing.tokenSymbol - : true - : true + | formatToken : item.pricing.tokenSymbol : true : true | async }}   ({{ unitsService.getUsd( - cartService.valueDivideExponent({ value: item.pricing.discountedPrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), + cartService.valueDivideExponent({ + value: item.pricing.discountedPrice || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), item.pricing.tokenSymbol ) | async @@ -162,7 +187,9 @@ -
+
@@ -228,7 +255,7 @@
- + {{ cartStatus.status }} @@ -243,7 +270,12 @@
- + Your Cart (change)="updateQuantity($event, item.nft.uid)" min="1" [max]="(cartService.getAvailableNftQuantity(item) | async) ?? 1" - [disabled]="((cartService.getAvailableNftQuantity(item) | async) ?? 1) <= 1"> + [disabled]=" + ((cartService.getAvailableNftQuantity(item) | async) ?? 1) <= 1 + " + /> - / {{ (cartService.getAvailableNftQuantity(item) | async) }} + / {{ cartService.getAvailableNftQuantity(item) | async }} @@ -273,15 +308,15 @@ > {{ item.pricing.originalPrice - | formatToken - : item.pricing.tokenSymbol - : true - : true + | formatToken : item.pricing.tokenSymbol : true : true | async }}   ({{ unitsService.getUsd( - cartService.valueDivideExponent({ value: item.pricing.originalPrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), + cartService.valueDivideExponent({ + value: item.pricing.originalPrice || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), item.pricing.tokenSymbol ) | async @@ -293,15 +328,15 @@
{{ item.pricing.discountedPrice - | formatToken - : item.pricing.tokenSymbol - : true - : true + | formatToken : item.pricing.tokenSymbol : true : true | async }}   ({{ unitsService.getUsd( - cartService.valueDivideExponent({ value: item.pricing.discountedPrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), + cartService.valueDivideExponent({ + value: item.pricing.discountedPrice || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), item.pricing.tokenSymbol ) | async @@ -322,7 +357,7 @@ nzType="default" [disabled]=" (cartService.getCurrentStep() !== stepType.CONFIRM && - cartService.getSelectedNetwork() === item.pricing.tokenSymbol) || + cartService.getSelectedNetwork() === item.pricing.tokenSymbol) || (isLoading$ | async) " (click)="removeFromCart(item)" @@ -330,12 +365,10 @@ [nzTooltipTitle]=" cartService.getCurrentStep() !== stepType.CONFIRM && cartService.getSelectedNetwork() === item.pricing.tokenSymbol - ? 'Item is part of a pending transaction. Please wait for the transaction to expire or be completed before removing this item from the cart.' - : ( - (isLoading$ | async) + ? 'Item is part of a pending transaction. Please wait for the transaction to expire or be completed before removing this item from the cart.' + : (isLoading$ | async) ? 'Cart items are loading. Please wait.' : '' - ) " > Your Cart
-
Your cart is empty.
@@ -368,7 +400,7 @@ nz-button nzType="default" (click)="clearCart()" - [disabled]="(isLoading$ | async)" + [disabled]="isLoading$ | async" nz-tooltip [nzTooltipTitle]="(isLoading$ | async) ? 'Waiting for cart items to finish loading' : ''" class="text-red-600 hover:text-red-800" diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.ts b/src/app/components/cart/components/cart-modal/cart-modal.component.ts index f68a42d..3a74f6d 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.ts +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.ts @@ -5,9 +5,7 @@ import { ChangeDetectorRef, ChangeDetectionStrategy, } from '@angular/core'; -import { - Network, -} from '@build-5/interfaces'; +import { Network } from '@build-5/interfaces'; import { Subscription, take, of, Observable } from 'rxjs'; import { CartService, CartItem } from '@components/cart/services/cart.service'; import { AuthService } from '@components/auth/services/auth.service'; @@ -30,7 +28,7 @@ export enum StepType { styleUrls: ['./cart-modal.component.less'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CartModalComponent implements OnInit, OnDestroy { +export class CartModalComponent implements OnDestroy { private subscriptions$ = new Subscription(); public collectionPath: string = ROUTER_UTILS.config.collection.root; public nftPath: string = ROUTER_UTILS.config.nft.root; @@ -60,11 +58,7 @@ export class CartModalComponent implements OnInit, OnDestroy { private router: Router, public unitsService: UnitsService, public deviceService: DeviceService, - ) { - - } - - ngOnInit() {} + ) {} trackByItemId(index: number, item: CartItem): string { return item.nft.uid; @@ -78,26 +72,29 @@ export class CartModalComponent implements OnInit, OnDestroy { const inputElement = event.target as HTMLInputElement; let newQuantity = Number(inputElement.value); - this.cartService.getCartItems().pipe( - take(1), - switchMap(cartItems => { - const item = cartItems.find(item => item.nft.uid === itemId); - if (item) { - return this.cartService.getAvailableNftQuantity(item).pipe( - map(maxQuantity => ({ item, maxQuantity })), - ); - } else { - return of(null); + this.cartService + .getCartItems() + .pipe( + take(1), + switchMap((cartItems) => { + const item = cartItems.find((item) => item.nft.uid === itemId); + if (item) { + return this.cartService + .getAvailableNftQuantity(item) + .pipe(map((maxQuantity) => ({ item, maxQuantity }))); + } else { + return of(null); + } + }), + ) + .subscribe((result) => { + if (result) { + const { maxQuantity } = result; + newQuantity = Math.max(1, Math.min(newQuantity, maxQuantity)); + inputElement.value = String(newQuantity); + this.cartService.updateCartItemQuantity(itemId, newQuantity); } - }), - ).subscribe(result => { - if (result) { - const { maxQuantity } = result; - newQuantity = Math.max(1, Math.min(newQuantity, maxQuantity)); - inputElement.value = String(newQuantity); - this.cartService.updateCartItemQuantity(itemId, newQuantity); - } - }); + }); } public handleClose(): void { diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.html b/src/app/components/cart/components/checkout/checkout-overlay.component.html index 4046326..43d6045 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.html +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.html @@ -33,8 +33,7 @@

- +

@@ -74,33 +73,33 @@

- - NFT: - - + NFT: + {{ item.nft.name }}
- - COL: - - + COL: + {{ item.collection.name }}
- - Royalties: - - - {{ (item.collection.royaltiesFee || 0) * 100 }}% - + Royalties: + {{ (item.collection.royaltiesFee || 0) * 100 }}%
+ + {{ item.quantity }} {{ @@ -116,7 +115,10 @@


({{ unitsService.getUsd( - cartService.valueDivideExponent({ value: item.salePrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), + cartService.valueDivideExponent({ + value: item.salePrice || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), group.network ) | async @@ -129,9 +131,7 @@

- - Total - + Total @@ -206,7 +206,10 @@

}}   ({{ unitsService.getUsd( - cartService.valueDivideExponent({ value: item.salePrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), + cartService.valueDivideExponent({ + value: item.salePrice || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), group.network ) | async @@ -228,7 +231,10 @@

}}   ({{ unitsService.getUsd( - cartService.valueDivideExponent({ value: (item.quantity * item.salePrice) || 0, exponents: cartService.getDefaultNetworkDecimals() }), + cartService.valueDivideExponent({ + value: item.quantity * item.salePrice || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), group.network ) | async @@ -256,7 +262,6 @@

-

diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.ts b/src/app/components/cart/components/checkout/checkout-overlay.component.ts index 678663e..694c170 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.ts +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.ts @@ -24,7 +24,15 @@ import { AuthService } from '@components/auth/services/auth.service'; import { NotificationService } from '@core/services/notification'; import { OrderApi } from '@api/order.api'; import { NftApi } from '@api/nft.api'; -import { BehaviorSubject, firstValueFrom, interval, Observable, Subscription, forkJoin, of } from 'rxjs'; +import { + BehaviorSubject, + firstValueFrom, + interval, + Observable, + Subscription, + forkJoin, + of, +} from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { TransactionService } from '@core/services/transaction'; import { @@ -406,14 +414,14 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { } public groupItems() { - const availabilityChecks$ = this.items.map(item => + const availabilityChecks$ = this.items.map((item) => this.cartService.isCartItemAvailableForSale(item).pipe( - map(result => ({ item, isAvailable: result.isAvailable })), - switchMap(result => result ? of(result) : of({ item, isAvailable: false })) - ) + map((result) => ({ item, isAvailable: result.isAvailable })), + switchMap((result) => (result ? of(result) : of({ item, isAvailable: false }))), + ), ); - forkJoin(availabilityChecks$).subscribe(results => { + forkJoin(availabilityChecks$).subscribe((results) => { const groups: { [tokenSymbol: string]: GroupedCartItem } = {}; this.unavailableItemCount = 0; @@ -424,7 +432,9 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { } const tokenSymbol = - (item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network) || DEFAULT_NETWORK; + (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) || DEFAULT_NETWORK; const discount = this.discount(item); const originalPrice = this.calcPrice(item, 1); const discountedPrice = this.calcPrice(item, discount); @@ -432,10 +442,18 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { item.salePrice = price; const network = - (item.nft?.placeholderNft ? item.collection?.mintingData?.network : item.nft?.mintingData?.network) || DEFAULT_NETWORK; + (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) || DEFAULT_NETWORK; if (!groups[tokenSymbol]) { - groups[tokenSymbol] = { tokenSymbol, items: [], totalQuantity: 0, totalPrice: 0, network }; + groups[tokenSymbol] = { + tokenSymbol, + items: [], + totalQuantity: 0, + totalPrice: 0, + network, + }; } groups[tokenSymbol].items.push(item); @@ -468,10 +486,11 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { } public isCartItemAvailableForSale(item: CartItem): Observable { - return this.cartService.isCartItemAvailableForSale(item).pipe(map(result => result.isAvailable)); + return this.cartService + .isCartItemAvailableForSale(item) + .pipe(map((result) => result.isAvailable)); } - public reset(): void { this.receivedTransactions = false; this.currentStep = StepType.CONFIRM; @@ -506,10 +525,9 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { } public goToMemberNfts(): void { - const memberId = this.auth.member$.value?.uid; - if(!memberId) { + if (!memberId) { return; } diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts index 4e4cc7c..7266e31 100644 --- a/src/app/components/cart/services/cart.service.ts +++ b/src/app/components/cart/services/cart.service.ts @@ -1,5 +1,16 @@ import { Injectable, NgZone } from '@angular/core'; -import { BehaviorSubject, Observable, Subscription, map, of, take, tap, catchError, finalize, combineLatest } from 'rxjs'; +import { + BehaviorSubject, + Observable, + Subscription, + map, + of, + take, + tap, + catchError, + finalize, + combineLatest, +} from 'rxjs'; import { Nft, Collection, @@ -127,22 +138,28 @@ export class CartService { } private loadMemberSpaces(memberId: string): void { - this.memberApi.allSpacesAsMember(memberId).pipe( - untilDestroyed(this), - map(spaces => spaces.map(space => space.uid)), - ).subscribe(spaceIds => { - this.memberSpacesSubject$.next(spaceIds); - spaceIds.forEach(spaceId => this.updateMemberGuardianStatus(spaceId)); - }); + this.memberApi + .allSpacesAsMember(memberId) + .pipe( + untilDestroyed(this), + map((spaces) => spaces.map((space) => space.uid)), + ) + .subscribe((spaceIds) => { + this.memberSpacesSubject$.next(spaceIds); + spaceIds.forEach((spaceId) => this.updateMemberGuardianStatus(spaceId)); + }); } private loadMemberAwards(memberId: string): void { - this.memberApi.topAwardsCompleted(memberId).pipe( - untilDestroyed(this), - map(awards => awards.map(award => award.uid)), - ).subscribe(awardIds => { - this.memberAwardsSubject$.next(awardIds); - }); + this.memberApi + .topAwardsCompleted(memberId) + .pipe( + untilDestroyed(this), + map((awards) => awards.map((award) => award.uid)), + ) + .subscribe((awardIds) => { + this.memberAwardsSubject$.next(awardIds); + }); } private updateMemberGuardianStatus(spaceId: string) { @@ -227,21 +244,23 @@ export class CartService { return; } - const freshDataObservables = cartItems.map(item => + const freshDataObservables = cartItems.map((item) => this.nftApi.getNftById(item.nft.uid).pipe( take(1), - map(freshNft => freshNft ? { ...item, nft: freshNft } : item), - catchError(() => of(item)) - ) + map((freshNft) => (freshNft ? { ...item, nft: freshNft } : item)), + catchError(() => of(item)), + ), ); - combineLatest(freshDataObservables).pipe( - take(1), - finalize(() => this.isLoadingSubject$.next(false)) - ).subscribe(updatedCartItems => { - this.cartItemsSubject$.next(updatedCartItems); - this.cartModalOpenSubject$.next(true); - }); + combineLatest(freshDataObservables) + .pipe( + take(1), + finalize(() => this.isLoadingSubject$.next(false)), + ) + .subscribe((updatedCartItems) => { + this.cartItemsSubject$.next(updatedCartItems); + this.cartModalOpenSubject$.next(true); + }); } public hideCartModal(): void { @@ -253,37 +272,50 @@ export class CartService { } public openCheckoutOverlay(): void { - if (!this.checkoutOverlayOpenSubject$.getValue()) { - const checkoutTransaction = getCheckoutTransaction(); if (checkoutTransaction && checkoutTransaction.transactionId) { if (checkoutTransaction.source === 'nftCheckout') { - this.notification.error('You currently have an open order. Pay for it or let it expire.', ''); + this.notification.error( + 'You currently have an open order. Pay for it or let it expire.', + '', + ); return; } if (checkoutTransaction.source === 'cartCheckout') { this.setCurrentTransaction(checkoutTransaction.transactionId); - this.getCurrentTransaction().pipe(take(1)).subscribe(currentTransaction => { - if (currentTransaction && currentTransaction.uid) { - const expiresOn: dayjs.Dayjs = dayjs(currentTransaction.createdOn!.toDate()).add(TRANSACTION_AUTO_EXPIRY_MS, 'ms'); - - if (expiresOn.isBefore(dayjs()) || currentTransaction.payload?.void || currentTransaction.payload?.reconciled) { - removeItem(StorageItem.CheckoutTransaction); - this.pendingTransaction$.next(undefined); - this.setCurrentStep(StepType.CONFIRM); - this.openModal(); + this.getCurrentTransaction() + .pipe(take(1)) + .subscribe((currentTransaction) => { + if (currentTransaction && currentTransaction.uid) { + const expiresOn: dayjs.Dayjs = dayjs(currentTransaction.createdOn!.toDate()).add( + TRANSACTION_AUTO_EXPIRY_MS, + 'ms', + ); + + if ( + expiresOn.isBefore(dayjs()) || + currentTransaction.payload?.void || + currentTransaction.payload?.reconciled + ) { + removeItem(StorageItem.CheckoutTransaction); + this.pendingTransaction$.next(undefined); + this.setCurrentStep(StepType.CONFIRM); + this.openModal(); + } else { + this.openModal(); + } } else { this.openModal(); } - } else { - this.openModal(); - } - }); + }); } else { - this.notification.error('CheckoutTransaction exists and source is not nftCheckout, the checkout-overlay is not open and the pending transaction not expired or complete and the source is not cartCheckout, this should never happen.', ''); + this.notification.error( + 'CheckoutTransaction exists and source is not nftCheckout, the checkout-overlay is not open and the pending transaction not expired or complete and the source is not cartCheckout, this should never happen.', + '', + ); return; } } else { @@ -293,27 +325,27 @@ export class CartService { } private openModal(): void { - this.getCartItems().pipe(take(1)).subscribe(cartItems => { - if (cartItems.length > 0) { - this.checkoutOverlayModalRef = this.modalService.create({ - nzTitle: 'Checkout', - nzContent: CheckoutOverlayComponent, - nzComponentParams: { items: cartItems }, - nzFooter: null, - nzWidth: '80%', - }); + this.getCartItems() + .pipe(take(1)) + .subscribe((cartItems) => { + if (cartItems.length > 0) { + this.checkoutOverlayModalRef = this.modalService.create({ + nzTitle: 'Checkout', + nzContent: CheckoutOverlayComponent, + nzComponentParams: { items: cartItems }, + nzFooter: null, + nzWidth: '80%', + }); - this.checkoutOverlayModalRef.afterClose.subscribe(() => { - this.closeCheckoutOverlay(); - }); + this.checkoutOverlayModalRef.afterClose.subscribe(() => { + this.closeCheckoutOverlay(); + }); - this.checkoutOverlayOpenSubject$.next(true); - } - }); + this.checkoutOverlayOpenSubject$.next(true); + } + }); } - - public closeCheckoutOverlay(): void { const checkoutTransaction = getCheckoutTransaction(); if (checkoutTransaction && checkoutTransaction.transactionId) { @@ -403,18 +435,17 @@ export class CartService { } public cartItemStatus(item: CartItem): Observable<{ status: string; message: string }> { - return this.isCartItemAvailableForSale(item) - .pipe( - map(availabilityResult => ({ - status: availabilityResult.isAvailable ? 'Available' : 'Not Available', - message: availabilityResult.message - })) - ); + return this.isCartItemAvailableForSale(item).pipe( + map((availabilityResult) => ({ + status: availabilityResult.isAvailable ? 'Available' : 'Not Available', + message: availabilityResult.message, + })), + ); } - public addToCart(nft: Nft, collection: Collection, quantity: number = 1): void { + public addToCart(nft: Nft, collection: Collection, quantity = 1): void { const currentItems = this.cartItemsSubject$.getValue(); - const existingItemIndex = currentItems.findIndex(item => item.nft.uid === nft.uid); + const existingItemIndex = currentItems.findIndex((item) => item.nft.uid === nft.uid); if (existingItemIndex > -1) { this.notification.error('This NFT already exists in your cart.', ''); @@ -424,7 +455,9 @@ export class CartService { const discountRate = this.discount(collection, nft); const originalPrice = this.calcPrice(nft, 1); const discountedPrice = this.calcPrice(nft, discountRate); - const tokenSymbol = (nft.placeholderNft ? collection.mintingData?.network : nft.mintingData?.network) || DEFAULT_NETWORK; + const tokenSymbol = + (nft.placeholderNft ? collection.mintingData?.network : nft.mintingData?.network) || + DEFAULT_NETWORK; const cartItem: CartItem = { nft: nft, @@ -442,10 +475,12 @@ export class CartService { this.cartItemsSubject$.next(updatedCartItems); this.saveCartItems(); - this.notification.success(`NFT ${nft.name} from collection ${collection.name} has been added to your cart.`, ''); + this.notification.success( + `NFT ${nft.name} from collection ${collection.name} has been added to your cart.`, + '', + ); } - public discount(collection?: Collection | null, nft?: Nft | null): number { if (!collection?.space || !this.auth.member$.value || nft?.owner) { return 1; @@ -514,21 +549,23 @@ export class CartService { } public updateCartItemQuantity(itemId: string, newQuantity: number): void { - this.cartItemsSubject$.pipe( - take(1), - tap(cartItems => { - const updatedCartItems = cartItems.map(item => { - if (item.nft.uid === itemId) { - return { ...item, quantity: newQuantity }; - } - return item; - }); + this.cartItemsSubject$ + .pipe( + take(1), + tap((cartItems) => { + const updatedCartItems = cartItems.map((item) => { + if (item.nft.uid === itemId) { + return { ...item, quantity: newQuantity }; + } + return item; + }); - this.cartItemsSubject$.next(updatedCartItems); + this.cartItemsSubject$.next(updatedCartItems); - this.saveCartItems(); - }) - ).subscribe(); + this.saveCartItems(); + }), + ) + .subscribe(); } public getSelectedNetwork(): any { @@ -538,7 +575,7 @@ export class CartService { public isNftAvailableForSale( nft: Nft, collection: Collection, - checkCartPresence: boolean = false + checkCartPresence = false, ): Observable<{ isAvailable: boolean; message: string }> { let message = 'NFT is available for sale.'; const conditions: string[] = []; @@ -550,13 +587,15 @@ export class CartService { const memberAwards = this.memberAwardsSubject$.getValue(); if (checkCartPresence) { - const isNftInCart = this.cartItemsSubject$.getValue().some(cartItem => cartItem.nft.uid === nft.uid); + const isNftInCart = this.cartItemsSubject$ + .getValue() + .some((cartItem) => cartItem.nft.uid === nft.uid); if (isNftInCart) { - message = 'This NFT is already in your cart.'; - return of({ - isAvailable, - message - }); + message = 'This NFT is already in your cart.'; + return of({ + isAvailable, + message, + }); } } @@ -564,7 +603,7 @@ export class CartService { message = 'Internal Error: Collection data is null or undefined.'; return of({ isAvailable, - message + message, }); } @@ -572,7 +611,7 @@ export class CartService { message = 'Internal Error: Nft and/or NFT Available From date is null or undefined.'; return of({ isAvailable, - message + message, }); } @@ -607,7 +646,11 @@ export class CartService { if (isOwner) conditions.push('You are the owner of this NFT.'); const availableValue = +nft?.available; - const nftAvailable = availableValue === 1 || availableValue === 3 || nft?.available === null || nft?.available === undefined; + const nftAvailable = + availableValue === 1 || + availableValue === 3 || + nft?.available === null || + nft?.available === undefined; if (!nftAvailable) conditions.push('NFT is not marked as available.'); const spaceMemberAccess = @@ -645,20 +688,22 @@ export class CartService { return of({ isAvailable, - message + message, }); } - public isCartItemAvailableForSale(cartItem: CartItem, checkCartPresence: boolean = false): Observable<{ isAvailable: boolean; message: string }> { - return this.isNftAvailableForSale(cartItem.nft, cartItem.collection, checkCartPresence) - .pipe( - map(result => { - return { - isAvailable: result.isAvailable, - message: result.message - }; - }) - ); + public isCartItemAvailableForSale( + cartItem: CartItem, + checkCartPresence = false, + ): Observable<{ isAvailable: boolean; message: string }> { + return this.isNftAvailableForSale(cartItem.nft, cartItem.collection, checkCartPresence).pipe( + map((result) => { + return { + isAvailable: result.isAvailable, + message: result.message, + }; + }), + ); } public clearCart(): void { @@ -667,17 +712,16 @@ export class CartService { this.notification.success($localize`All items have been removed from your cart.`, ''); } - public getAvailableNftQuantity(cartItem: CartItem): Observable { return this.isCartItemAvailableForSale(cartItem).pipe( - map(result => { + map((result) => { if (result.isAvailable) { - return cartItem.nft.placeholderNft ? (cartItem.collection.availableNfts || 0) : 1; + return cartItem.nft.placeholderNft ? cartItem.collection.availableNfts || 0 : 1; } else { return 0; } }), - map(quantity => quantity === null ? 0 : quantity) + map((quantity) => (quantity === null ? 0 : quantity)), ); } @@ -701,5 +745,4 @@ export class CartService { public getDefaultNetworkDecimals(): number { return DEFAULT_NETWORK_DECIMALS; } - } diff --git a/src/app/components/nft/components/nft-card/nft-card.component.html b/src/app/components/nft/components/nft-card/nft-card.component.html index 7302f3b..a4665c1 100644 --- a/src/app/components/nft/components/nft-card/nft-card.component.html +++ b/src/app/components/nft/components/nft-card/nft-card.component.html @@ -240,7 +240,12 @@ - + + +
Your NFTs will be locked for purchase for {{ lockTime }} minutes. diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.ts b/src/app/components/cart/components/checkout/checkout-overlay.component.ts index 694c170..7014169 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.ts +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.ts @@ -117,7 +117,7 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { constructor( public cartService: CartService, - private auth: AuthService, + public auth: AuthService, private notification: NotificationService, private orderApi: OrderApi, public transactionService: TransactionService, diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts index 7266e31..718a4b9 100644 --- a/src/app/components/cart/services/cart.service.ts +++ b/src/app/components/cart/services/cart.service.ts @@ -22,6 +22,8 @@ import { CollectionStatus, DEFAULT_NETWORK_DECIMALS, Network, + COL, + Timestamp, } from '@build-5/interfaces'; import { getItem, removeItem, StorageItem, getCheckoutTransaction } from '@core/utils'; import { NzNotificationService } from 'ng-zorro-antd/notification'; @@ -35,6 +37,9 @@ import { MemberApi } from '@api/member.api'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import dayjs from 'dayjs'; import { NftApi } from '@api/nft.api'; +import { FilterStorageService } from '@core/services/filter-storage'; +import { AlgoliaService } from '@components/algolia/services/algolia.service'; +import { InstantSearchConfig } from 'angular-instantsearch/instantsearch/instantsearch'; export interface CartItem { nft: Nft; @@ -81,9 +86,11 @@ export class CartService { private memberSpacesSubject$ = new BehaviorSubject([]); private memberGuardianSpacesSubject$ = new BehaviorSubject([]); private memberAwardsSubject$ = new BehaviorSubject([]); + private memberNftCollectionIdsSubject$ = new BehaviorSubject([]); private guardianSpaceSubscriptions$: { [key: string]: Subscription } = {}; private isLoadingSubject$ = new BehaviorSubject(false); public isLoading$ = this.isLoadingSubject$.asObservable(); + public config?: InstantSearchConfig; constructor( private notification: NzNotificationService, @@ -95,6 +102,8 @@ export class CartService { private memberApi: MemberApi, private zone: NgZone, private nftApi: NftApi, + public filterStorageService: FilterStorageService, + public readonly algoliaService: AlgoliaService, ) { this.subscribeToMemberChanges(); this.listenToStorageChanges(); @@ -122,11 +131,13 @@ export class CartService { } private refreshMemberData() { - if (this.auth.member$.value?.uid) { - this.loadMemberSpaces(this.auth.member$.value?.uid); - this.loadMemberAwards(this.auth.member$.value?.uid); + const memberId = this.auth.member$.value?.uid; + if (memberId) { + this.loadMemberSpaces(memberId); + this.loadMemberAwards(memberId); + this.loadMemberNfts(memberId); } else { - this.resetMemberData(); + this.resetMemberData(); } } @@ -134,6 +145,7 @@ export class CartService { this.memberSpacesSubject$.next([]); this.memberGuardianSpacesSubject$.next([]); this.memberAwardsSubject$.next([]); + this.memberNftCollectionIdsSubject$.next([]); this.cleanupGuardianSubscriptions(); } @@ -162,6 +174,28 @@ export class CartService { }); } + private loadMemberNfts(memberId: string): void { + this.algoliaService.fetchAllOwnedNfts(memberId, COL.NFT).then(nfts => { + const results = this.convertAllToSoonaverseModel(nfts); + + const collectionIds = results.map(hit => hit.collection) + .filter((value, index, self) => self.indexOf(value) === index); + const currentIds = this.memberNftCollectionIdsSubject$.getValue(); + const allIds = [...new Set([...currentIds, ...collectionIds])]; + this.memberNftCollectionIdsSubject$.next(allIds); + }).catch(error => { + console.error('Error fetching owned NFTs:', error); + }); + + } + + public convertAllToSoonaverseModel(algoliaItems: any[]) { + return algoliaItems.map((algolia) => ({ + ...algolia, + availableFrom: Timestamp.fromMillis(+algolia.availableFrom), + })); + } + private updateMemberGuardianStatus(spaceId: string) { if (this.auth.member$.value?.uid) { this.guardianSpaceSubscriptions$[spaceId]?.unsubscribe(); @@ -443,8 +477,14 @@ export class CartService { ); } - public addToCart(nft: Nft, collection: Collection, quantity = 1): void { + public addToCart(nft: Nft, collection: Collection, quantity = 1, hideNotif = false): void { const currentItems = this.cartItemsSubject$.getValue(); + + if (currentItems.length >= 100) { + this.notification.error('You cannot add more than 100 unique NFTs to your cart.', ''); + return; + } + const existingItemIndex = currentItems.findIndex((item) => item.nft.uid === nft.uid); if (existingItemIndex > -1) { @@ -475,10 +515,12 @@ export class CartService { this.cartItemsSubject$.next(updatedCartItems); this.saveCartItems(); - this.notification.success( - `NFT ${nft.name} from collection ${collection.name} has been added to your cart.`, - '', - ); + if (hideNotif === false) { + this.notification.success( + `NFT ${nft.name} from collection ${collection.name} has been added to your cart.`, + '', + ); + } } public discount(collection?: Collection | null, nft?: Nft | null): number { @@ -522,10 +564,6 @@ export class CartService { ); this.cartItemsSubject$.next(updatedCartItems); this.saveCartItems(); - this.notification.success( - $localize`NFT ${cartItem.nft.name} from collection ${cartItem.collection.name} has been removed from your cart.`, - '', - ); } public removeItemsFromCart(itemIds: string[]): void { @@ -576,6 +614,7 @@ export class CartService { nft: Nft, collection: Collection, checkCartPresence = false, + checkPendingTransaction = true, ): Observable<{ isAvailable: boolean; message: string }> { let message = 'NFT is available for sale.'; const conditions: string[] = []; @@ -585,6 +624,18 @@ export class CartService { const memberSpaces = this.memberSpacesSubject$.getValue(); const memberGuardianSpaces = this.memberGuardianSpacesSubject$.getValue(); const memberAwards = this.memberAwardsSubject$.getValue(); + const memberNftCollectionIds = this.memberNftCollectionIdsSubject$.getValue(); + + if (checkPendingTransaction) { + const pendingTrx = this.hasPendingTransaction(); + if (pendingTrx) { + message = 'Finish pending cart checkout transaction or wait for it to expire before adding more items to cart.'; + return of({ + isAvailable, + message, + }); + } + } if (checkCartPresence) { const isNftInCart = this.cartItemsSubject$ @@ -653,21 +704,34 @@ export class CartService { nft?.available === undefined; if (!nftAvailable) conditions.push('NFT is not marked as available.'); - const spaceMemberAccess = - collection?.access !== 1 || - (collection?.access === 1 && memberSpaces.includes(collection?.space ?? '')); - if (!spaceMemberAccess) conditions.push('Member does not have access to this space.'); - - const spaceGuardianAccess = - collection?.access !== 2 || - (collection?.access === 2 && memberGuardianSpaces.includes(collection?.space ?? '')); - if (!spaceGuardianAccess) conditions.push('Member is not a guardian of this space.'); - - const spaceAwardAccess = - collection?.access !== 3 || - (collection?.access === 3 && - collection?.accessAwards?.some((award) => memberAwards.includes(award))); - if (!spaceAwardAccess) conditions.push('Member does not have the required awards for access.'); + let spaceMemberAccess = true; + let spaceGuardianAccess = true; + let spaceAwardAccess = true; + let nftOwnedAccess = true; + + if (nft?.isOwned === false) { + spaceMemberAccess = + collection?.access !== 1 || + (collection?.access === 1 && memberSpaces.includes(collection?.space ?? '')); + if (!spaceMemberAccess) conditions.push('Member does not have access to this space.'); + + spaceGuardianAccess = + collection?.access !== 2 || + (collection?.access === 2 && memberGuardianSpaces.includes(collection?.space ?? '')); + if (!spaceGuardianAccess) conditions.push('Member is not a guardian of this space.'); + + spaceAwardAccess = + collection?.access !== 3 || + (collection?.access === 3 && + collection?.accessAwards?.some((award) => memberAwards.includes(award))); + if (!spaceAwardAccess) conditions.push('Member does not have the required awards for access.'); + + nftOwnedAccess = + collection?.access !== 4 || + (collection?.access === 4 && + collection?.accessCollections?.every((coll) => memberNftCollectionIds.includes(coll))); + if (!nftOwnedAccess) conditions.push('Member does not own at least one NFT from each of the required access collections.'); + } isAvailable = !collectionStatusMinting && @@ -679,7 +743,8 @@ export class CartService { nftAvailable && spaceMemberAccess && spaceGuardianAccess && - spaceAwardAccess; + spaceAwardAccess && + nftOwnedAccess; if (!isAvailable && conditions.length > 0) { message = @@ -696,7 +761,7 @@ export class CartService { cartItem: CartItem, checkCartPresence = false, ): Observable<{ isAvailable: boolean; message: string }> { - return this.isNftAvailableForSale(cartItem.nft, cartItem.collection, checkCartPresence).pipe( + return this.isNftAvailableForSale(cartItem.nft, cartItem.collection, checkCartPresence, false).pipe( map((result) => { return { isAvailable: result.isAvailable, diff --git a/src/app/components/nft/components/nft-card/nft-card.component.ts b/src/app/components/nft/components/nft-card/nft-card.component.ts index 5fc984f..e85bed5 100644 --- a/src/app/components/nft/components/nft-card/nft-card.component.ts +++ b/src/app/components/nft/components/nft-card/nft-card.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { FileApi } from '@api/file.api'; import { MemberApi } from '@api/member.api'; @@ -31,7 +31,8 @@ import { BehaviorSubject, Subscription, take } from 'rxjs'; styleUrls: ['./nft-card.component.less'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class NftCardComponent { +export class NftCardComponent implements OnInit, OnDestroy { + private cartSubscription$!: Subscription; @Input() fullWidth?: boolean; @Input() enableWithdraw?: boolean; @@ -93,6 +94,13 @@ export class NftCardComponent { public cartService: CartService, ) {} + ngOnInit(): void { + this.cartSubscription$ = this.cartService.getCartItems().subscribe(() => { + this.cd.markForCheck(); + }); + } + + public onBuy(event: MouseEvent): void { event.stopPropagation(); event.preventDefault(); @@ -199,4 +207,8 @@ export class NftCardComponent { console.error('Attempted to add a null or undefined NFT or Collection to the cart'); } } + + ngOnDestroy() { + this.cartSubscription$.unsubscribe(); + } } diff --git a/src/app/pages/collection/pages/collection/nfts/nfts.page.ts b/src/app/pages/collection/pages/collection/nfts/nfts.page.ts index bddc459..49e65be 100644 --- a/src/app/pages/collection/pages/collection/nfts/nfts.page.ts +++ b/src/app/pages/collection/pages/collection/nfts/nfts.page.ts @@ -207,7 +207,7 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { .subscribe({ next: ({ nftsToAdd, collection }) => { nftsToAdd.forEach((nft) => { - this.cartService.addToCart(nft, collection); + this.cartService.addToCart(nft, collection, 1, true); }); this.notification.success( $localize`NFTs swept into your cart, open cart to review added items.`, @@ -217,6 +217,8 @@ export class CollectionNFTsPage implements OnInit, OnChanges, OnDestroy { error: (error) => this.notification.error($localize`Error occurred while adding NFTs to cart.`, ''), }); + + this.cd.markForCheck(); } public ngOnDestroy(): void { diff --git a/src/app/pages/member/pages/nfts/nfts.page.ts b/src/app/pages/member/pages/nfts/nfts.page.ts index 3d76ac3..5f759f0 100644 --- a/src/app/pages/member/pages/nfts/nfts.page.ts +++ b/src/app/pages/member/pages/nfts/nfts.page.ts @@ -80,7 +80,7 @@ export class NFTsPage implements OnInit { } public convertAllToSoonaverseModel(algoliaItems: any[]) { - return algoliaItems.map((algolia) => ({ + return algoliaItems.map((algolia) => ({ ...algolia, availableFrom: Timestamp.fromMillis(+algolia.availableFrom), })); From 6f1a25b7344cdd58894b8358ef293db09e6566de Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Fri, 16 Feb 2024 20:01:50 -0500 Subject: [PATCH 15/24] prettier/lint commit --- src/app/@shell/ui/header/header.component.ts | 4 +- .../algolia/services/algolia.service.ts | 29 +++++---- src/app/components/cart/cart.module.ts | 8 +-- .../cart-modal/cart-modal.component.ts | 16 +++-- .../checkout/checkout-overlay.component.html | 30 +++++++++- .../components/cart/services/cart.service.ts | 59 +++++++++++-------- .../components/nft-card/nft-card.component.ts | 10 +++- src/app/pages/member/pages/nfts/nfts.page.ts | 2 +- 8 files changed, 102 insertions(+), 56 deletions(-) diff --git a/src/app/@shell/ui/header/header.component.ts b/src/app/@shell/ui/header/header.component.ts index 2ef69fe..e85c05c 100644 --- a/src/app/@shell/ui/header/header.component.ts +++ b/src/app/@shell/ui/header/header.component.ts @@ -133,7 +133,7 @@ export class HeaderComponent implements OnInit, OnDestroy { this.cartItemsSubscription$ = this.cartService.getCartItems().subscribe((items) => { let count = 0; items.forEach((nft) => { - count += nft.quantity + count += nft.quantity; }); this.cartItemCount = count; }); @@ -247,7 +247,7 @@ export class HeaderComponent implements OnInit, OnDestroy { this.cartItemsSubscription$ = this.cartService.getCartItems().subscribe((items) => { let count = 0; items.forEach((nft) => { - count += nft.quantity + count += nft.quantity; }); this.cartItemCount = count; }); diff --git a/src/app/components/algolia/services/algolia.service.ts b/src/app/components/algolia/services/algolia.service.ts index 06b52b5..07c63f0 100644 --- a/src/app/components/algolia/services/algolia.service.ts +++ b/src/app/components/algolia/services/algolia.service.ts @@ -35,19 +35,22 @@ export class AlgoliaService { return new Promise((resolve, reject) => { const fetchPage = () => { - index.search('', { - filters: `owner:${memberId}`, - hitsPerPage, - page, - }).then(response => { - allHits.push(...response.hits); - if (page < response.nbPages - 1) { - page++; - fetchPage(); - } else { - resolve(allHits); - } - }).catch(reject); + index + .search('', { + filters: `owner:${memberId}`, + hitsPerPage, + page, + }) + .then((response) => { + allHits.push(...response.hits); + if (page < response.nbPages - 1) { + page++; + fetchPage(); + } else { + resolve(allHits); + } + }) + .catch(reject); }; fetchPage(); diff --git a/src/app/components/cart/cart.module.ts b/src/app/components/cart/cart.module.ts index d68391d..46e41f8 100644 --- a/src/app/components/cart/cart.module.ts +++ b/src/app/components/cart/cart.module.ts @@ -26,8 +26,8 @@ import { NzRadioModule } from 'ng-zorro-antd/radio'; import { UsdBelowTwoDecimalsModule } from '@core/pipes/usd-below-two-decimals/usd-below-two-decimals.module'; import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; import { ConnectWalletModule } from '@components/connect-wallet/connect-wallet.module'; -//import { DataService } from '@pages/member/services/data.service'; -//import { AlgoliaModule } from '@components/algolia/algolia.module'; +// import { DataService } from '@pages/member/services/data.service'; +// import { AlgoliaModule } from '@components/algolia/algolia.module'; @NgModule({ declarations: [CartModalComponent, CheckoutOverlayComponent], @@ -58,8 +58,8 @@ import { ConnectWalletModule } from '@components/connect-wallet/connect-wallet.m UsdBelowTwoDecimalsModule, NzToolTipModule, ConnectWalletModule, - //DataService, - //AlgoliaModule, + // DataService, + // AlgoliaModule, ], exports: [CartModalComponent, CheckoutOverlayComponent], }) diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.ts b/src/app/components/cart/components/cart-modal/cart-modal.component.ts index 2296869..6b8b5e4 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.ts +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.ts @@ -81,15 +81,13 @@ export class CartModalComponent implements OnDestroy { switchMap((cartItems) => { const item = cartItems.find((item) => item.nft.uid === itemId); if (item) { - return this.cartService - .getAvailableNftQuantity(item) - .pipe( - map((maxQuantity) => ({ item, maxQuantity })), - tap(({ maxQuantity }) => { - newQuantity = Math.min(newQuantity, maxQuantity); - inputElement.value = String(newQuantity); - }) - ); + return this.cartService.getAvailableNftQuantity(item).pipe( + map((maxQuantity) => ({ item, maxQuantity })), + tap(({ maxQuantity }) => { + newQuantity = Math.min(newQuantity, maxQuantity); + inputElement.value = String(newQuantity); + }), + ); } else { return of(null); } diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.html b/src/app/components/cart/components/checkout/checkout-overlay.component.html index c0e2c69..fcdff46 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.html +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.html @@ -26,7 +26,21 @@

Network: {{ group.tokenSymbol }} - - {{ group.items.length }} unique NFTs in network group with total Price of {{ group.totalPrice | formatToken : group.network : true : true | async }} ({{ unitsService.getUsd(cartService.valueDivideExponent({ value: group.totalPrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), group.network) | async | currency : 'USD' | UsdBelowTwoDecimals }} USD) + {{ group.items.length }} unique NFTs in network group + with total Price of + {{ group.totalPrice | formatToken : group.network : true : true | async }} ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ + value: group.totalPrice || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), + group.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }} + USD)

@@ -270,7 +284,19 @@

{{ group.totalPrice | formatToken : group.network : true : true | async }} -   ({{ unitsService.getUsd(cartService.valueDivideExponent({ value: group.totalPrice || 0, exponents: cartService.getDefaultNetworkDecimals() }), group.network) | async | currency : 'USD' | UsdBelowTwoDecimals }} USD) +   ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ + value: group.totalPrice || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), + group.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }} + USD) diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts index 718a4b9..9643403 100644 --- a/src/app/components/cart/services/cart.service.ts +++ b/src/app/components/cart/services/cart.service.ts @@ -133,11 +133,11 @@ export class CartService { private refreshMemberData() { const memberId = this.auth.member$.value?.uid; if (memberId) { - this.loadMemberSpaces(memberId); - this.loadMemberAwards(memberId); - this.loadMemberNfts(memberId); + this.loadMemberSpaces(memberId); + this.loadMemberAwards(memberId); + this.loadMemberNfts(memberId); } else { - this.resetMemberData(); + this.resetMemberData(); } } @@ -175,18 +175,21 @@ export class CartService { } private loadMemberNfts(memberId: string): void { - this.algoliaService.fetchAllOwnedNfts(memberId, COL.NFT).then(nfts => { - const results = this.convertAllToSoonaverseModel(nfts); - - const collectionIds = results.map(hit => hit.collection) - .filter((value, index, self) => self.indexOf(value) === index); - const currentIds = this.memberNftCollectionIdsSubject$.getValue(); - const allIds = [...new Set([...currentIds, ...collectionIds])]; - this.memberNftCollectionIdsSubject$.next(allIds); - }).catch(error => { - console.error('Error fetching owned NFTs:', error); - }); - + this.algoliaService + .fetchAllOwnedNfts(memberId, COL.NFT) + .then((nfts) => { + const results = this.convertAllToSoonaverseModel(nfts); + + const collectionIds = results + .map((hit) => hit.collection) + .filter((value, index, self) => self.indexOf(value) === index); + const currentIds = this.memberNftCollectionIdsSubject$.getValue(); + const allIds = [...new Set([...currentIds, ...collectionIds])]; + this.memberNftCollectionIdsSubject$.next(allIds); + }) + .catch((error) => { + console.error('Error fetching owned NFTs:', error); + }); } public convertAllToSoonaverseModel(algoliaItems: any[]) { @@ -629,7 +632,8 @@ export class CartService { if (checkPendingTransaction) { const pendingTrx = this.hasPendingTransaction(); if (pendingTrx) { - message = 'Finish pending cart checkout transaction or wait for it to expire before adding more items to cart.'; + message = + 'Finish pending cart checkout transaction or wait for it to expire before adding more items to cart.'; return of({ isAvailable, message, @@ -724,13 +728,17 @@ export class CartService { collection?.access !== 3 || (collection?.access === 3 && collection?.accessAwards?.some((award) => memberAwards.includes(award))); - if (!spaceAwardAccess) conditions.push('Member does not have the required awards for access.'); + if (!spaceAwardAccess) + conditions.push('Member does not have the required awards for access.'); nftOwnedAccess = - collection?.access !== 4 || - (collection?.access === 4 && - collection?.accessCollections?.every((coll) => memberNftCollectionIds.includes(coll))); - if (!nftOwnedAccess) conditions.push('Member does not own at least one NFT from each of the required access collections.'); + collection?.access !== 4 || + (collection?.access === 4 && + collection?.accessCollections?.every((coll) => memberNftCollectionIds.includes(coll))); + if (!nftOwnedAccess) + conditions.push( + 'Member does not own at least one NFT from each of the required access collections.', + ); } isAvailable = @@ -761,7 +769,12 @@ export class CartService { cartItem: CartItem, checkCartPresence = false, ): Observable<{ isAvailable: boolean; message: string }> { - return this.isNftAvailableForSale(cartItem.nft, cartItem.collection, checkCartPresence, false).pipe( + return this.isNftAvailableForSale( + cartItem.nft, + cartItem.collection, + checkCartPresence, + false, + ).pipe( map((result) => { return { isAvailable: result.isAvailable, diff --git a/src/app/components/nft/components/nft-card/nft-card.component.ts b/src/app/components/nft/components/nft-card/nft-card.component.ts index e85bed5..2ca245b 100644 --- a/src/app/components/nft/components/nft-card/nft-card.component.ts +++ b/src/app/components/nft/components/nft-card/nft-card.component.ts @@ -1,4 +1,11 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; import { Router } from '@angular/router'; import { FileApi } from '@api/file.api'; import { MemberApi } from '@api/member.api'; @@ -100,7 +107,6 @@ export class NftCardComponent implements OnInit, OnDestroy { }); } - public onBuy(event: MouseEvent): void { event.stopPropagation(); event.preventDefault(); diff --git a/src/app/pages/member/pages/nfts/nfts.page.ts b/src/app/pages/member/pages/nfts/nfts.page.ts index 5f759f0..3d76ac3 100644 --- a/src/app/pages/member/pages/nfts/nfts.page.ts +++ b/src/app/pages/member/pages/nfts/nfts.page.ts @@ -80,7 +80,7 @@ export class NFTsPage implements OnInit { } public convertAllToSoonaverseModel(algoliaItems: any[]) { - return algoliaItems.map((algolia) => ({ + return algoliaItems.map((algolia) => ({ ...algolia, availableFrom: Timestamp.fromMillis(+algolia.availableFrom), })); From a905a88960a48d575c3446b906d4cecccc9bb3ec Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Wed, 21 Feb 2024 23:22:58 -0500 Subject: [PATCH 16/24] NFT checkout styling fixes, USD conversions added in more places, extended cart qty input width, add multiple NFTs to cart from NFT page --- .../cart-modal/cart-modal.component.html | 8 +- .../nft-checkout/nft-checkout.component.html | 268 +++++++++++++----- .../nft-checkout/nft-checkout.component.less | 3 + .../nft-checkout/nft-checkout.component.ts | 50 ++-- .../nft-checkout/nft-checkout.module.ts | 2 + src/app/pages/nft/pages/nft/nft.page.html | 23 +- src/app/pages/nft/pages/nft/nft.page.ts | 7 +- src/app/pages/nft/services/helper.service.ts | 12 - 8 files changed, 259 insertions(+), 114 deletions(-) diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.html b/src/app/components/cart/components/cart-modal/cart-modal.component.html index c7c5273..c365131 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.html +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.html @@ -98,7 +98,7 @@
-
Qty Added / Available
+
Qty / Available
Your Cart

cartService.getAvailableNftQuantity(item) | async }}" > - + Your Cart
-
+
Your Cart cartService.getAvailableNftQuantity(item) | async }}" > - + -
-
+
+
-
+ +

{{ getTitle() }}

{{ collection?.name }}
@@ -65,25 +66,11 @@

{{ getTitle() }}

- -
- You have selected to perform a bulk order of this NFT. Please review quantity - selected before finalizing and purchase. -
-
- -
-
Total price @@ -95,6 +82,21 @@

{{ getTitle() }}

>
{{ targetPrice | formatToken : collection?.mintingData?.network : true | async }} +
+ + ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ + value: targetPrice || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), + collection?.mintingData?.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }}) +
{{ @@ -102,6 +104,21 @@

{{ getTitle() }}

| formatToken : collection?.mintingData?.network : true | async }} +
+ + ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ + value: calc(targetPrice, discount()) || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), + collection?.mintingData?.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }}) +
@@ -111,10 +128,25 @@

{{ getTitle() }}

>
{{ - currentStep !== stepType.CONFIRM - ? (targetAmount | formatToken : collection?.mintingData?.network : true | async) - : (targetPrice | formatToken : collection?.mintingData?.network : true | async) + (currentStep !== stepType.CONFIRM ? targetAmount : targetPrice) + | formatToken : collection?.mintingData?.network : true + | async }} +
+ + ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ + value: currentStep !== stepType.CONFIRM ? targetAmount : targetPrice || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), + collection?.mintingData?.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }}) +
@@ -128,48 +160,104 @@

{{ getTitle() }}

else elseBlock " > +
+
+ +
+ + You have selected to perform a bulk order of this NFT. Please review quantity + selected before finalizing and purchase. + +
+
+
- Price each + Price
-
-
+
{{ targetPrice | formatToken : collection?.mintingData?.network : true | async }} +
+ + ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ + value: targetPrice || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), + collection?.mintingData?.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }}) +
-
+
{{ calc(targetPrice, discount()) | formatToken : collection?.mintingData?.network : true | async }} +
+ + ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ + value: calc(targetPrice, discount()) || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), + collection?.mintingData?.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }}) +
-
-
+
{{ - currentStep !== stepType.CONFIRM - ? (pricePerItem - | formatToken : collection?.mintingData?.network : true - | async) - : (targetPrice - | formatToken : collection?.mintingData?.network : true - | async) + (currentStep !== stepType.CONFIRM ? pricePerItem : targetPrice) + | formatToken : collection?.mintingData?.network : true + | async }} +
+ + ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ + value: + currentStep !== stepType.CONFIRM ? pricePerItem : targetPrice || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), + collection?.mintingData?.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }}) +
@@ -177,14 +265,15 @@

{{ getTitle() }}

- Order quantity + Quantity
-
-
- {{ nftQuantity }} +
+
+ {{ nftQuantity }}
+  
@@ -192,49 +281,96 @@

{{ getTitle() }}

- Total price + Total
-
-
+
{{ targetPrice * nftQuantity | formatToken : collection?.mintingData?.network : true | async }} +
+ + ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ + value: targetPrice * nftQuantity || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), + collection?.mintingData?.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }}) +
-
+
{{ calc(targetPrice, discount()) * nftQuantity | formatToken : collection?.mintingData?.network : true | async }} +
+ + ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ + value: calc(targetPrice, discount()) * nftQuantity || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), + collection?.mintingData?.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }}) +
-
-
+
{{ - currentStep !== stepType.CONFIRM && targetAmount !== null - ? (targetAmount - | formatToken : collection?.mintingData?.network : true - | async) - : (targetPrice * nftQuantity - | formatToken : collection?.mintingData?.network : true - | async) + (currentStep !== stepType.CONFIRM && targetAmount !== null + ? targetAmount + : targetPrice * nftQuantity + ) + | formatToken : collection?.mintingData?.network : true + | async }} +
+ + ({{ + unitsService.getUsd( + cartService.valueDivideExponent({ + value: + currentStep !== stepType.CONFIRM && targetAmount !== null + ? targetAmount + : targetPrice * nftQuantity || 0, + exponents: cartService.getDefaultNetworkDecimals() + }), + collection?.mintingData?.network + ) + | async + | currency : 'USD' + | UsdBelowTwoDecimals + }}) +
@@ -244,11 +380,11 @@

{{ getTitle() }}

@@ -260,7 +396,7 @@

{{ getTitle() }}

Target Price @@ -274,7 +410,7 @@

{{ getTitle() }}

Target Amount @@ -288,7 +424,7 @@

{{ getTitle() }}

Quantity Selected @@ -302,7 +438,7 @@

{{ getTitle() }}

Discount @@ -316,7 +452,7 @@

{{ getTitle() }}

Purchase Workflow Step diff --git a/src/app/components/nft/components/nft-checkout/nft-checkout.component.less b/src/app/components/nft/components/nft-checkout/nft-checkout.component.less index e69de29..dd760e2 100644 --- a/src/app/components/nft/components/nft-checkout/nft-checkout.component.less +++ b/src/app/components/nft/components/nft-checkout/nft-checkout.component.less @@ -0,0 +1,3 @@ +.alignCenter { + text-align: center; +} 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 ad5d8a8..b4b457d 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 @@ -21,9 +21,7 @@ import { PreviewImageService } from '@core/services/preview-image'; import { TransactionService } from '@core/services/transaction'; import { UnitsService } from '@core/services/units'; import { - getItem, removeItem, - setItem, StorageItem, setCheckoutTransaction, getCheckoutTransaction, @@ -45,6 +43,7 @@ import { } from '@build-5/interfaces'; import dayjs from 'dayjs'; import { BehaviorSubject, firstValueFrom, interval, Subscription, take } from 'rxjs'; +import { CartService } from '@components/cart/services/cart.service'; export enum StepType { CONFIRM = 'Confirm', @@ -72,7 +71,6 @@ 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); } @@ -95,7 +93,6 @@ 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); } } } @@ -165,17 +162,16 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { private nftApi: NftApi, private fileApi: FileApi, private cache: CacheService, + public cartService: CartService, ) {} 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. @@ -457,9 +453,12 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { 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 + this.transSubscription$?.unsubscribe(); + setCheckoutTransaction({ + transactionId: val.uid, + source: 'nftCheckout', + }); + this.transSubscription$ = this.orderApi .listen(val.uid) .subscribe(this.transaction$); this.pushToHistory(val, val.uid, dayjs(), $localize`Waiting for transaction...`); @@ -474,23 +473,26 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { params.nft = this.nft.uid; } - if (this.nft.owner) { - params.nft = this.nft.uid; - } + 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(); - setCheckoutTransaction({ - transactionId: val.uid, - source: 'nftCheckout', + await this.auth.sign(params, (sc, finish) => { + this.notification + .processRequest(this.orderApi.orderNft(sc), $localize`Order created.`, finish) + .subscribe((val: any) => { + this.transSubscription$?.unsubscribe(); + setCheckoutTransaction({ + transactionId: val.uid, + source: 'nftCheckout', + }); + this.transSubscription$ = this.orderApi + .listen(val.uid) + .subscribe(this.transaction$); + this.pushToHistory(val, val.uid, dayjs(), $localize`Waiting for transaction...`); }); - 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/components/nft/components/nft-checkout/nft-checkout.module.ts b/src/app/components/nft/components/nft-checkout/nft-checkout.module.ts index bad391e..20e441d 100644 --- a/src/app/components/nft/components/nft-checkout/nft-checkout.module.ts +++ b/src/app/components/nft/components/nft-checkout/nft-checkout.module.ts @@ -19,6 +19,7 @@ import { NzModalModule } from 'ng-zorro-antd/modal'; import { NzNotificationModule } from 'ng-zorro-antd/notification'; import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; import { NftCheckoutComponent } from './nft-checkout.component'; +import { UsdBelowTwoDecimalsModule } from '@core/pipes/usd-below-two-decimals/usd-below-two-decimals.module'; @NgModule({ declarations: [NftCheckoutComponent], @@ -42,6 +43,7 @@ import { NftCheckoutComponent } from './nft-checkout.component'; ModalDrawerModule, WalletDeeplinkModule, TermsAndConditionsModule, + UsdBelowTwoDecimalsModule, ], exports: [NftCheckoutComponent], }) diff --git a/src/app/pages/nft/pages/nft/nft.page.html b/src/app/pages/nft/pages/nft/nft.page.html index 1d0889e..8f1e23a 100644 --- a/src/app/pages/nft/pages/nft/nft.page.html +++ b/src/app/pages/nft/pages/nft/nft.page.html @@ -77,24 +77,35 @@

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

Current price
- {{(((data.nft$ | async)?.availablePrice || (data.nft$ | async)?.price) | + {{ (((data.nft$ | async)?.availablePrice || (data.nft$ | async)?.price) | formatToken: ((data.nft$ | async)?.placeholderNft ? (data.collection$ | - async)?.mintingData?.network : (data.nft$ | async)?.mintingData?.network): true | - async)}} + async)?.mintingData?.network : (data.nft$ | async)?.mintingData?.network):true) | + async }} ({{ unitsService.getUsd(cartService.valueDivideExponent({ value: + ((data.nft$ | async)?.availablePrice || (data.nft$ | async)?.price) || 0, exponents: + cartService.getDefaultNetworkDecimals() }), ((data.nft$ | async)?.placeholderNft ? + (data.collection$ | async)?.mintingData?.network : (data.nft$ | + async)?.mintingData?.network)) | async | currency: 'USD':'symbol':'1.0-2' }} USD)
- {{(calc(((data.nft$ | async)?.availablePrice || (data.nft$ | async)?.price), + {{ (calc(((data.nft$ | async)?.availablePrice || (data.nft$ | async)?.price), discount(data.collection$ | async, data.nft$ | async)) | formatToken: ((data.nft$ | async)?.placeholderNft ? (data.collection$ | async)?.mintingData?.network : - (data.nft$ | async)?.mintingData?.network): true | async)}} + (data.nft$ | async)?.mintingData?.network):true) | async }} ({{ + unitsService.getUsd(cartService.valueDivideExponent({ value: calc(((data.nft$ | + async)?.availablePrice || (data.nft$ | async)?.price), discount(data.collection$ | + async, data.nft$ | async)) || 0, exponents: cartService.getDefaultNetworkDecimals() + }), ((data.nft$ | async)?.placeholderNft ? (data.collection$ | + async)?.mintingData?.network : (data.nft$ | async)?.mintingData?.network)) | async | + currency: 'USD':'symbol':'1.0-2' }} USD)
+
-
diff --git a/src/app/pages/nft/pages/nft/nft.page.ts b/src/app/pages/nft/pages/nft/nft.page.ts index bf89c84..be55bf2 100644 --- a/src/app/pages/nft/pages/nft/nft.page.ts +++ b/src/app/pages/nft/pages/nft/nft.page.ts @@ -443,6 +443,7 @@ export class NFTPage implements OnInit, OnDestroy { this.data.collection$.value, this.data.nft$.value, ); + const startingValue = this.nftQtySelected; const parsedQuantity = Math.round(Number(this.nftQtySelected)); if (isNaN(parsedQuantity) || parsedQuantity <= 0) { @@ -453,7 +454,9 @@ export class NFTPage implements OnInit, OnDestroy { this.nftQtySelected = parsedQuantity; } - if (parsedQuantity === this.nftQtySelected) { + this.cd.markForCheck(); + + if (startingValue === this.nftQtySelected) { return; } else { this.resetInput(); @@ -613,7 +616,7 @@ export class NFTPage implements OnInit, OnDestroy { if (nft && this.data.collection$) { this.data.collection$.pipe(take(1)).subscribe((collection) => { if (collection) { - this.cartService.addToCart(nft, collection); + this.cartService.addToCart(nft, collection, this.nftQtySelected); } }); } diff --git a/src/app/pages/nft/services/helper.service.ts b/src/app/pages/nft/services/helper.service.ts index 9cf076f..b7a6cea 100644 --- a/src/app/pages/nft/services/helper.service.ts +++ b/src/app/pages/nft/services/helper.service.ts @@ -217,18 +217,6 @@ export class HelperService { return 0; } - 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; From 387e755840033975c3aa6933a7dbc09b1d426d97 Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Sun, 25 Feb 2024 17:13:29 -0500 Subject: [PATCH 17/24] Persisted selected network and current step with local storage to allow real time updates for these across browser tabs --- .../checkout/checkout-overlay.component.ts | 39 +++++++++++++++---- .../components/cart/services/cart.service.ts | 34 ++++++++++++++-- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.ts b/src/app/components/cart/components/checkout/checkout-overlay.component.ts index 7014169..40811e5 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.ts +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.ts @@ -7,6 +7,7 @@ import { Output, EventEmitter, OnDestroy, + NgZone, } from '@angular/core'; import { CartItem, CartService } from '@components/cart/services/cart.service'; import { @@ -129,6 +130,7 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { public themeService: ThemeService, public unitsService: UnitsService, public deviceService: DeviceService, + private zone: NgZone, ) {} public get themes(): typeof ThemeList { @@ -139,6 +141,7 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { this.subscribeToCurrentStep(); this.subscribeToCurrentTransaction(); this.subscribeToCartItems(); + this.listenToStorageForStepChanges(); this.groupItems(); this.receivedTransactions = false; @@ -288,6 +291,11 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { markInvalid(); } + this.cartService.selectedNetwork$.subscribe(network => { + this.selectedNetwork = network; + this.cd.markForCheck(); + }); + this.cd.markForCheck(); }); @@ -322,13 +330,11 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { }); if (this.currentStep === StepType.CONFIRM) { - this.clearNetworkSelection(); - if (this.groupedCartItems.length === 1) { - this.setNetworkSelection(this.groupedCartItems[0].tokenSymbol); + this.cartService.setNetworkSelection(this.groupedCartItems[0].tokenSymbol); } } else { - const storedNetwork = localStorage.getItem('cartCheckoutSelectedNetwork'); + const storedNetwork = this.cartService.getSelectedNetwork(); if (storedNetwork) { this.selectedNetwork = storedNetwork; } @@ -336,6 +342,20 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { this.setDefaultGroupVisibility(); } + private listenToStorageForStepChanges() { + window.addEventListener('storage', (event) => { + if (event.key === 'cartCheckoutCurrentStep') { + const updatedStep = event.newValue as StepType; + if (updatedStep && updatedStep !== this.currentStep) { + this.zone.run(() => { + this.currentStep = updatedStep; + this.cd.markForCheck(); + }); + } + } + }); + } + private subscribeToCartItems() { this.cartService .getCartItems() @@ -402,14 +422,14 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { public setNetworkSelection(networkSymbol: string): void { this.selectedNetwork = networkSymbol; - localStorage.setItem('cartCheckoutSelectedNetwork', networkSymbol); + this.cartService.setNetworkSelection(networkSymbol); this.expandedGroups.clear(); this.expandedGroups.add(networkSymbol); this.cd.markForCheck(); } public clearNetworkSelection(): void { - localStorage.removeItem('cartCheckoutSelectedNetwork'); + this.cartService.clearNetworkSelection(); this.selectedNetwork = null; } @@ -473,7 +493,7 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { private removePurchasedGroupItems(): void { if (this.selectedNetwork) { this.cartService.removeGroupItemsFromCart(this.selectedNetwork); - this.clearNetworkSelection(); + this.cartService.clearNetworkSelection(); } } @@ -631,9 +651,11 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { } public async proceedWithBulkOrder(nfts: NftPurchaseRequest[]): Promise { + const selectedGroup = this.groupedCartItems.find( (group) => group.tokenSymbol === this.selectedNetwork, ); + if (!selectedGroup) { this.nzNotification.error( $localize`No network selected or no items in the selected network.`, @@ -692,11 +714,12 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { } public getSelectedNetwork(): any { - const selectedNetwork = localStorage.getItem('cartCheckoutSelectedNetwork') || ''; + const selectedNetwork = this.cartService.getSelectedNetwork(); return selectedNetwork; } ngOnDestroy() { this.currentTransactionSubscription?.unsubscribe(); + window.removeEventListener('storage', this.listenToStorageForStepChanges); } } diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts index 9643403..946aa09 100644 --- a/src/app/components/cart/services/cart.service.ts +++ b/src/app/components/cart/services/cart.service.ts @@ -91,6 +91,8 @@ export class CartService { private isLoadingSubject$ = new BehaviorSubject(false); public isLoading$ = this.isLoadingSubject$.asObservable(); public config?: InstantSearchConfig; + private selectedNetworkSubject$ = new BehaviorSubject(this.getSelectedNetwork()); + public selectedNetwork$ = this.selectedNetworkSubject$.asObservable(); constructor( private notification: NzNotificationService, @@ -107,6 +109,7 @@ export class CartService { ) { this.subscribeToMemberChanges(); this.listenToStorageChanges(); + this.listenToNetworkSelectionChanges(); } private listenToStorageChanges(): void { @@ -120,6 +123,17 @@ export class CartService { }); } + private listenToNetworkSelectionChanges(): void { + window.addEventListener('storage', (event) => { + if (event.storageArea === localStorage && event.key === 'cartCheckoutSelectedNetwork') { + this.zone.run(() => { + const updatedNetwork = localStorage.getItem('cartCheckoutSelectedNetwork'); + this.selectedNetworkSubject$.next(updatedNetwork); + }); + } + }); + } + private subscribeToMemberChanges() { this.auth.member$.pipe(untilDestroyed(this)).subscribe((member) => { if (member) { @@ -265,6 +279,7 @@ export class CartService { public setCurrentStep(step: StepType): void { this.currentStepSubject$.next(step); + localStorage.setItem('cartCheckoutCurrentStep', step); } public getCurrentStep(): StepType { @@ -609,10 +624,6 @@ export class CartService { .subscribe(); } - public getSelectedNetwork(): any { - return localStorage.getItem('cartCheckoutSelectedNetwork') || ''; - } - public isNftAvailableForSale( nft: Nft, collection: Collection, @@ -823,4 +834,19 @@ export class CartService { public getDefaultNetworkDecimals(): number { return DEFAULT_NETWORK_DECIMALS; } + + public getSelectedNetwork(): string | null { + const selectedNetwork = localStorage.getItem('cartCheckoutSelectedNetwork'); + return selectedNetwork; + } + + public setNetworkSelection(networkSymbol: string): void { + localStorage.setItem('cartCheckoutSelectedNetwork', networkSymbol); + this.selectedNetworkSubject$.next(networkSymbol); + } + + public clearNetworkSelection(): void { + localStorage.removeItem('cartCheckoutSelectedNetwork'); + this.selectedNetworkSubject$.next(null); + } } From 0d33e8105a19526a300010c3d84263e9582e7dfa Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Sun, 25 Feb 2024 17:18:44 -0500 Subject: [PATCH 18/24] lint/prettier commit --- .../cart/components/checkout/checkout-overlay.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.ts b/src/app/components/cart/components/checkout/checkout-overlay.component.ts index 40811e5..4b78109 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.ts +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.ts @@ -291,7 +291,7 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { markInvalid(); } - this.cartService.selectedNetwork$.subscribe(network => { + this.cartService.selectedNetwork$.subscribe((network) => { this.selectedNetwork = network; this.cd.markForCheck(); }); @@ -651,7 +651,6 @@ export class CheckoutOverlayComponent implements OnInit, OnDestroy { } public async proceedWithBulkOrder(nfts: NftPurchaseRequest[]): Promise { - const selectedGroup = this.groupedCartItems.find( (group) => group.tokenSymbol === this.selectedNetwork, ); From 49bba175daa80f8f450ce6ef178106df4881b2a1 Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Wed, 28 Feb 2024 00:34:24 -0500 Subject: [PATCH 19/24] Cart item remove button disable logic now includes expiry of pending purchase transaction plus some minor UI styling tweaks. --- .../cart-modal/cart-modal.component.html | 2 ++ .../cart-modal/cart-modal.component.ts | 14 ++++----- .../checkout/checkout-overlay.component.html | 2 +- .../components/cart/services/cart.service.ts | 2 +- .../nft-checkout/nft-checkout.component.html | 30 +++++++++---------- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.html b/src/app/components/cart/components/cart-modal/cart-modal.component.html index c365131..4368b04 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.html +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.html @@ -357,6 +357,7 @@ nzType="default" [disabled]=" (cartService.getCurrentStep() !== stepType.CONFIRM && + !helper.isExpired(cartService.pendingTransaction$ | async) && cartService.getSelectedNetwork() === item.pricing.tokenSymbol) || (isLoading$ | async) " @@ -364,6 +365,7 @@ nz-tooltip [nzTooltipTitle]=" cartService.getCurrentStep() !== stepType.CONFIRM && + !helper.isExpired(cartService.pendingTransaction$ | async) && cartService.getSelectedNetwork() === item.pricing.tokenSymbol ? 'Item is part of a pending transaction. Please wait for the transaction to expire or be completed before removing this item from the cart.' : (isLoading$ | async) diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.ts b/src/app/components/cart/components/cart-modal/cart-modal.component.ts index 6b8b5e4..e384b01 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.ts +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.ts @@ -1,12 +1,6 @@ -import { - Component, - OnInit, - OnDestroy, - ChangeDetectorRef, - ChangeDetectionStrategy, -} from '@angular/core'; -import { Network } from '@build-5/interfaces'; -import { Subscription, take, of, Observable } from 'rxjs'; +import { Component, OnDestroy, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'; +import { Network, Transaction } from '@build-5/interfaces'; +import { Subscription, take, of, Observable, BehaviorSubject } from 'rxjs'; import { CartService, CartItem } from '@components/cart/services/cart.service'; import { AuthService } from '@components/auth/services/auth.service'; import { Router } from '@angular/router'; @@ -14,6 +8,7 @@ import { ROUTER_UTILS } from '@core/utils/router.utils'; import { UnitsService } from '@core/services/units/units.service'; import { map, switchMap, tap } from 'rxjs/operators'; import { DeviceService } from '@core/services/device'; +import { HelperService } from '@pages/nft/services/helper.service'; export enum StepType { CONFIRM = 'Confirm', @@ -58,6 +53,7 @@ export class CartModalComponent implements OnDestroy { private router: Router, public unitsService: UnitsService, public deviceService: DeviceService, + public helper: HelperService, ) {} trackByItemId(index: number, item: CartItem): string { diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.html b/src/app/components/cart/components/checkout/checkout-overlay.component.html index fcdff46..931e8f0 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.html +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.html @@ -310,7 +310,7 @@

-
+
diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts index 946aa09..9609f92 100644 --- a/src/app/components/cart/services/cart.service.ts +++ b/src/app/components/cart/services/cart.service.ts @@ -80,7 +80,7 @@ export class CartService { private currentStepSubject$ = new BehaviorSubject(StepType.CONFIRM); public currentStep$ = this.currentStepSubject$.asObservable(); private checkoutOverlayModalRef: NzModalRef | null = null; - private pendingTransaction$: BehaviorSubject = new BehaviorSubject< + public pendingTransaction$: BehaviorSubject = new BehaviorSubject< Transaction | undefined >(undefined); private memberSpacesSubject$ = new BehaviorSubject([]); 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 cd13dce..3c7e742 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 @@ -70,7 +70,7 @@

{{ getTitle() }}

>
Total price @@ -178,7 +178,7 @@

{{ getTitle() }}

Price @@ -188,7 +188,7 @@

{{ getTitle() }}

*ngIf="discount() < 1 && currentStep === stepType.CONFIRM" >
{{ targetPrice | formatToken : collection?.mintingData?.network : true | async @@ -209,7 +209,7 @@

{{ getTitle() }}

}})
-
+
{{ calc(targetPrice, discount()) | formatToken : collection?.mintingData?.network : true @@ -236,7 +236,7 @@

{{ getTitle() }}

class="flex items-center mt-2 text-sm" *ngIf="discount() === 1 || currentStep !== stepType.CONFIRM" > -
+
{{ (currentStep !== stepType.CONFIRM ? pricePerItem : targetPrice) | formatToken : collection?.mintingData?.network : true @@ -265,7 +265,7 @@

{{ getTitle() }}

Quantity @@ -281,7 +281,7 @@

{{ getTitle() }}

Total @@ -291,7 +291,7 @@

{{ getTitle() }}

*ngIf="discount() < 1 && currentStep === stepType.CONFIRM && targetPrice !== null" >
{{ targetPrice * nftQuantity @@ -314,7 +314,7 @@

{{ getTitle() }}

}})
-
+
{{ calc(targetPrice, discount()) * nftQuantity | formatToken : collection?.mintingData?.network : true @@ -344,7 +344,7 @@

{{ getTitle() }}

(currentStep !== stepType.CONFIRM && targetAmount !== null) " > -
+
{{ (currentStep !== stepType.CONFIRM && targetAmount !== null ? targetAmount @@ -396,7 +396,7 @@

{{ getTitle() }}

Target Price @@ -410,7 +410,7 @@

{{ getTitle() }}

Target Amount @@ -424,7 +424,7 @@

{{ getTitle() }}

Quantity Selected @@ -438,7 +438,7 @@

{{ getTitle() }}

Discount @@ -452,7 +452,7 @@

{{ getTitle() }}

Purchase Workflow Step From 5d984e409fd8ae21b24cfd6fb9b56757d424fe0d Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Sun, 3 Mar 2024 05:11:44 -0500 Subject: [PATCH 20/24] refactored cross tab checkout state management --- .../cart-modal/cart-modal.component.html | 20 +- .../cart-modal/cart-modal.component.ts | 61 +++- .../checkout/checkout-overlay.component.html | 2 +- .../components/cart/services/cart.service.ts | 270 +++++++++++------- 4 files changed, 236 insertions(+), 117 deletions(-) diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.html b/src/app/components/cart/components/cart-modal/cart-modal.component.html index 4368b04..a8ffc07 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.html +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.html @@ -356,17 +356,17 @@ nz-button nzType="default" [disabled]=" - (cartService.getCurrentStep() !== stepType.CONFIRM && - !helper.isExpired(cartService.pendingTransaction$ | async) && - cartService.getSelectedNetwork() === item.pricing.tokenSymbol) || + ((cartService.pendingTransaction$ | async) && + !helper.isExpired(cartService.pendingTransaction$ | async) && + (cartService.selectedNetwork$ | async) === item.pricing.tokenSymbol) || (isLoading$ | async) " (click)="removeFromCart(item)" nz-tooltip [nzTooltipTitle]=" - cartService.getCurrentStep() !== stepType.CONFIRM && + (cartService.pendingTransaction$ | async) && !helper.isExpired(cartService.pendingTransaction$ | async) && - cartService.getSelectedNetwork() === item.pricing.tokenSymbol + (cartService.selectedNetwork$ | async) === item.pricing.tokenSymbol ? 'Item is part of a pending transaction. Please wait for the transaction to expire or be completed before removing this item from the cart.' : (isLoading$ | async) ? 'Cart items are loading. Please wait.' @@ -383,6 +383,11 @@
Your cart is empty. +
+ Pending Transaction: {{ (cartService.pendingTransaction$ | async) }} +
+ Transaction Expired: {{ helper.isExpired(cartService.pendingTransaction$ | async) }} +
@@ -398,11 +403,11 @@

Checkout
+
diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.ts b/src/app/components/cart/components/cart-modal/cart-modal.component.ts index e384b01..e78cc7d 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.ts +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.ts @@ -1,6 +1,6 @@ -import { Component, OnDestroy, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'; +import { Component, OnDestroy, ChangeDetectorRef, ChangeDetectionStrategy, OnInit } from '@angular/core'; import { Network, Transaction } from '@build-5/interfaces'; -import { Subscription, take, of, Observable, BehaviorSubject } from 'rxjs'; +import { Subscription, take, of, Observable, BehaviorSubject, Subject } from 'rxjs'; import { CartService, CartItem } from '@components/cart/services/cart.service'; import { AuthService } from '@components/auth/services/auth.service'; import { Router } from '@angular/router'; @@ -9,6 +9,7 @@ import { UnitsService } from '@core/services/units/units.service'; import { map, switchMap, tap } from 'rxjs/operators'; import { DeviceService } from '@core/services/device'; import { HelperService } from '@pages/nft/services/helper.service'; +import { } from '@angular/core'; export enum StepType { CONFIRM = 'Confirm', @@ -23,7 +24,7 @@ export enum StepType { styleUrls: ['./cart-modal.component.less'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CartModalComponent implements OnDestroy { +export class CartModalComponent implements OnInit, OnDestroy { private subscriptions$ = new Subscription(); public collectionPath: string = ROUTER_UTILS.config.collection.root; public nftPath: string = ROUTER_UTILS.config.nft.root; @@ -40,12 +41,20 @@ export class CartModalComponent implements OnDestroy { public cartModalOpen$ = this.cartService.cartModalOpen$; public currentStep$ = this.cartService.currentStep$; + public selectedNetwork$ = this.cartService.selectedNetwork$; + public memberSpaces$ = this.cartService.memberSpaces$; public memberGuardianSpaces$ = this.cartService.memberGuardianSpaces$; public memberAwards$ = this.cartService.memberAwards$; public isLoading$ = this.cartService.isLoading$; + public currentStep: StepType | null = null; + public isTransactionExpired: boolean | null = null; + public selectedNetwork: string | null = null; + public isLoading: boolean = false; + public pendingTransaction: Transaction | undefined = undefined; + constructor( public cartService: CartService, private cd: ChangeDetectorRef, @@ -56,6 +65,52 @@ export class CartModalComponent implements OnDestroy { public helper: HelperService, ) {} + ngOnInit() { + this.subscriptions$.add( + this.cartService.currentStep$.subscribe(step => { + this.currentStep = step; + this.triggerChangeDetection(); + }) + ); + + this.subscriptions$.add( + this.cartService.selectedNetwork$.subscribe(network => { + this.selectedNetwork = network; + this.triggerChangeDetection(); + }) + ); + + this.subscriptions$.add( + this.cartService.pendingTransaction$.subscribe(transaction => { + this.pendingTransaction = transaction; + this.triggerChangeDetection(); + }) + ); + + this.subscriptions$.add( + this.cartService.isLoading$.subscribe(loading => { + this.isLoading = loading; + this.triggerChangeDetection(); + }) + ); + + this.subscriptions$.add( + this.cartService.cartUpdateObservable$.subscribe(() => { + this.triggerChangeDetection(); + }) + ); + + this.subscriptions$.add( + this.cartService.triggerChangeDetectionSubject$.subscribe(() => { + this.triggerChangeDetection(); + }) + ); + } + + public triggerChangeDetection(): void { + this.cd.markForCheck(); + } + trackByItemId(index: number, item: CartItem): string { return item.nft.uid; } diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.html b/src/app/components/cart/components/checkout/checkout-overlay.component.html index 931e8f0..a701645 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.html +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.html @@ -26,7 +26,7 @@

Network: {{ group.tokenSymbol }} - - {{ group.items.length }} unique NFTs in network group + {{ group.totalQuantity }} NFTs in network group with total Price of {{ group.totalPrice | formatToken : group.network : true : true | async }} ({{ unitsService.getUsd( diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts index 9609f92..3124690 100644 --- a/src/app/components/cart/services/cart.service.ts +++ b/src/app/components/cart/services/cart.service.ts @@ -10,6 +10,7 @@ import { catchError, finalize, combineLatest, + Subject, } from 'rxjs'; import { Nft, @@ -66,6 +67,9 @@ export enum StepType { } export const CART_STORAGE_KEY = 'App/cartItems'; +export const NETWORK_STORAGE_KEY = 'cartCheckoutSelectedNetwork'; +export const STEP_STORAGE_KEY = 'cartCheckoutCurrentStep'; +export const TRAN_STORAGE_KEY = 'App/checkoutTransaction'; @Injectable({ providedIn: 'root', @@ -93,6 +97,10 @@ export class CartService { public config?: InstantSearchConfig; private selectedNetworkSubject$ = new BehaviorSubject(this.getSelectedNetwork()); public selectedNetwork$ = this.selectedNetworkSubject$.asObservable(); + private cartUpdateSubject$ = new Subject(); + public cartUpdateObservable$ = this.cartUpdateSubject$.asObservable(); + public triggerChangeDetectionSubject$ = new Subject(); + private transactionCheckInterval: any = null; constructor( private notification: NzNotificationService, @@ -109,29 +117,170 @@ export class CartService { ) { this.subscribeToMemberChanges(); this.listenToStorageChanges(); - this.listenToNetworkSelectionChanges(); } private listenToStorageChanges(): void { window.addEventListener('storage', (event) => { - if (event.storageArea === localStorage && event.key === CART_STORAGE_KEY) { - this.zone.run(() => { - const updatedCartItems = JSON.parse(event.newValue || '[]'); - this.cartItemsSubject$.next(updatedCartItems); - }); - } + if (event.storageArea === localStorage) { + this.zone.run(() => { + switch (event.key) { + case CART_STORAGE_KEY: + const updatedCartItems = JSON.parse(event.newValue || '[]'); + this.cartItemsSubject$.next(updatedCartItems); + break; + case NETWORK_STORAGE_KEY: + const updatedNetwork = JSON.parse(event.newValue || 'null'); + this.selectedNetworkSubject$.next(updatedNetwork); + break; + case STEP_STORAGE_KEY: + const newStep = JSON.parse(event.newValue || 'null'); + this.currentStepSubject$.next(newStep); + break; + case TRAN_STORAGE_KEY: + const newTran = JSON.parse(event.newValue || 'null'); + this.pendingTransaction$.next(newTran); + break; + } + this.cartUpdateSubject$.next(); + }); + } }); } - private listenToNetworkSelectionChanges(): void { - window.addEventListener('storage', (event) => { - if (event.storageArea === localStorage && event.key === 'cartCheckoutSelectedNetwork') { - this.zone.run(() => { - const updatedNetwork = localStorage.getItem('cartCheckoutSelectedNetwork'); - this.selectedNetworkSubject$.next(updatedNetwork); - }); + public startTransactionExpiryCheck(): void { + if (this.transactionCheckInterval) { + clearInterval(this.transactionCheckInterval); + } + + this.transactionCheckInterval = setInterval(() => { + + const transaction = this.pendingTransaction$.getValue(); + const currentStep = this.getCurrentStep(); + if (currentStep === StepType.TRANSACTION || currentStep === StepType.WAIT) { + if (transaction && transaction.uid) { + const expiresOn: dayjs.Dayjs = dayjs(transaction.createdOn!.toDate()).add( + TRANSACTION_AUTO_EXPIRY_MS, + 'ms', + ); + this.triggerChangeDetectionSubject$.next(); + + if ( + expiresOn.isBefore(dayjs()) || + transaction.payload?.void || + transaction.payload?.reconciled + ) { + removeItem(StorageItem.CheckoutTransaction); + this.pendingTransaction$.next(undefined); + this.triggerChangeDetectionSubject$.next(); + clearInterval(this.transactionCheckInterval); + this.transactionCheckInterval = null; + } + } else { + removeItem(StorageItem.CheckoutTransaction); + this.pendingTransaction$.next(undefined); + this.triggerChangeDetectionSubject$.next(); + clearInterval(this.transactionCheckInterval); + this.transactionCheckInterval = null; + } } - }); + }, 1000); + } + + public getSelectedNetwork(): string | null { + const selectedNetwork = localStorage.getItem(NETWORK_STORAGE_KEY); + return selectedNetwork ? JSON.parse(selectedNetwork) : null; + } + + public setNetworkSelection(networkSymbol: string): void { + const newNetwork = JSON.stringify(networkSymbol); + localStorage.setItem(NETWORK_STORAGE_KEY, newNetwork); + this.selectedNetworkSubject$.next(networkSymbol); + this.triggerChangeDetectionSubject$.next(); + } + + public clearNetworkSelection(): void { + localStorage.removeItem(NETWORK_STORAGE_KEY); + this.selectedNetworkSubject$.next(null); + } + + public getDefaultNetworkDecimals(): number { + return DEFAULT_NETWORK_DECIMALS; + } + + public setCurrentStep(step: StepType): void { + const newStep = JSON.stringify(step); + localStorage.setItem(STEP_STORAGE_KEY, newStep); + this.currentStepSubject$.next(step); + this.triggerChangeDetectionSubject$.next(); + } + + public getCurrentStep(): StepType { + const stepValue = localStorage.getItem(STEP_STORAGE_KEY); + return stepValue ? JSON.parse(stepValue) : StepType.CONFIRM; + } + + public setCurrentTransaction(transactionId: string): void { + if (transactionId === null || transactionId === undefined) { + return; + } + + this.orderApi + .listen(transactionId) + .pipe(untilDestroyed(this)) + .subscribe((transaction) => { + this.pendingTransaction$.next(transaction); + this.startTransactionExpiryCheck(); + this.triggerChangeDetectionSubject$.next(); + }); + } + + public getCurrentTransaction(): Observable { + return this.pendingTransaction$.asObservable(); + } + + public hasPendingTransaction(): boolean { + const checkoutTransaction = getCheckoutTransaction(); + const transactionId = checkoutTransaction?.transactionId; + return !!transactionId; + } + + public saveCartItems(): void { + localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(this.cartItemsSubject$.getValue())); + } + + public loadCartItems(): CartItem[] { + const items = getItem(StorageItem.CartItems) as CartItem[]; + return items || []; + } + + public getCartItems(): Observable { + return this.cartItemsSubject$.asObservable(); + } + + public clearCart(): void { + this.cartItemsSubject$.next([]); + this.saveCartItems(); + this.notification.success($localize`All items have been removed from your cart.`, ''); + } + + public refreshCartItems(): void { + this.cartItemsSubject$.next(this.cartItemsSubject$.value); + } + + public removeFromCart(cartItem: CartItem): void { + const updatedCartItems = this.cartItemsSubject$.value.filter( + (item) => item.nft.uid !== cartItem.nft.uid, + ); + this.cartItemsSubject$.next(updatedCartItems); + this.saveCartItems(); + } + + public removeItemsFromCart(itemIds: string[]): void { + const updatedCartItems = this.cartItemsSubject$.value.filter( + (item) => !itemIds.includes(item.nft.uid), + ); + this.cartItemsSubject$.next(updatedCartItems); + this.saveCartItems(); } private subscribeToMemberChanges() { @@ -254,38 +403,6 @@ export class CartService { return this.memberAwardsSubject$.asObservable(); } - public hasPendingTransaction(): boolean { - const checkoutTransaction = getCheckoutTransaction(); - const transactionId = checkoutTransaction?.transactionId; - return !!transactionId; - } - - public setCurrentTransaction(transactionId: string): void { - if (transactionId === null || transactionId === undefined) { - return; - } - - this.orderApi - .listen(transactionId) - .pipe(untilDestroyed(this)) - .subscribe((transaction) => { - this.pendingTransaction$.next(transaction); - }); - } - - public getCurrentTransaction(): Observable { - return this.pendingTransaction$.asObservable(); - } - - public setCurrentStep(step: StepType): void { - this.currentStepSubject$.next(step); - localStorage.setItem('cartCheckoutCurrentStep', step); - } - - public getCurrentStep(): StepType { - return this.currentStepSubject$.getValue(); - } - public showCartModal(): void { this.isLoadingSubject$.next(true); @@ -482,10 +599,6 @@ export class CartService { } } - public getCartItems(): Observable { - return this.cartItemsSubject$.asObservable(); - } - public cartItemStatus(item: CartItem): Observable<{ status: string; message: string }> { return this.isCartItemAvailableForSale(item).pipe( map((availabilityResult) => ({ @@ -572,26 +685,6 @@ export class CartService { return this.calc(itemPrice, discount); } - public refreshCartItems(): void { - this.cartItemsSubject$.next(this.cartItemsSubject$.value); - } - - public removeFromCart(cartItem: CartItem): void { - const updatedCartItems = this.cartItemsSubject$.value.filter( - (item) => item.nft.uid !== cartItem.nft.uid, - ); - this.cartItemsSubject$.next(updatedCartItems); - this.saveCartItems(); - } - - public removeItemsFromCart(itemIds: string[]): void { - const updatedCartItems = this.cartItemsSubject$.value.filter( - (item) => !itemIds.includes(item.nft.uid), - ); - this.cartItemsSubject$.next(updatedCartItems); - this.saveCartItems(); - } - public removeGroupItemsFromCart(tokenSymbol: string): void { const updatedCartItems = this.cartItemsSubject$.value.filter((item) => { const itemTokenSymbol = @@ -617,7 +710,6 @@ export class CartService { }); this.cartItemsSubject$.next(updatedCartItems); - this.saveCartItems(); }), ) @@ -795,12 +887,6 @@ export class CartService { ); } - public clearCart(): void { - this.cartItemsSubject$.next([]); - this.saveCartItems(); - this.notification.success($localize`All items have been removed from your cart.`, ''); - } - public getAvailableNftQuantity(cartItem: CartItem): Observable { return this.isCartItemAvailableForSale(cartItem).pipe( map((result) => { @@ -814,15 +900,6 @@ export class CartService { ); } - public saveCartItems(): void { - localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(this.cartItemsSubject$.getValue())); - } - - public loadCartItems(): CartItem[] { - const items = getItem(StorageItem.CartItems) as CartItem[]; - return items || []; - } - public valueDivideExponent(value: ConvertValue): number { if (value.exponents === 0 || value.value === null || value.value === undefined) { return value.value!; @@ -830,23 +907,4 @@ export class CartService { return value.value! / Math.pow(10, getDefDecimalIfNotSet(value.exponents)); } } - - public getDefaultNetworkDecimals(): number { - return DEFAULT_NETWORK_DECIMALS; - } - - public getSelectedNetwork(): string | null { - const selectedNetwork = localStorage.getItem('cartCheckoutSelectedNetwork'); - return selectedNetwork; - } - - public setNetworkSelection(networkSymbol: string): void { - localStorage.setItem('cartCheckoutSelectedNetwork', networkSymbol); - this.selectedNetworkSubject$.next(networkSymbol); - } - - public clearNetworkSelection(): void { - localStorage.removeItem('cartCheckoutSelectedNetwork'); - this.selectedNetworkSubject$.next(null); - } } From 3ab2ab3b3c1aec1e1a085ae07761bec6c47a6e64 Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Sun, 3 Mar 2024 05:20:24 -0500 Subject: [PATCH 21/24] lint/prettier --- .../cart-modal/cart-modal.component.html | 13 +++-- .../cart-modal/cart-modal.component.ts | 50 +++++++++-------- .../checkout/checkout-overlay.component.html | 4 +- .../components/cart/services/cart.service.ts | 53 ++++++++++--------- 4 files changed, 66 insertions(+), 54 deletions(-) diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.html b/src/app/components/cart/components/cart-modal/cart-modal.component.html index a8ffc07..882b497 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.html +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.html @@ -357,8 +357,8 @@ nzType="default" [disabled]=" ((cartService.pendingTransaction$ | async) && - !helper.isExpired(cartService.pendingTransaction$ | async) && - (cartService.selectedNetwork$ | async) === item.pricing.tokenSymbol) || + !helper.isExpired(cartService.pendingTransaction$ | async) && + (cartService.selectedNetwork$ | async) === item.pricing.tokenSymbol) || (isLoading$ | async) " (click)="removeFromCart(item)" @@ -384,7 +384,7 @@

Your cart is empty.
- Pending Transaction: {{ (cartService.pendingTransaction$ | async) }} + Pending Transaction: {{ cartService.pendingTransaction$ | async }}
Transaction Expired: {{ helper.isExpired(cartService.pendingTransaction$ | async) }}
@@ -407,7 +407,11 @@ nz-button nzType="default" (click)="clearCart()" - [disabled]="(cartService.pendingTransaction$ | async) && !helper.isExpired(cartService.pendingTransaction$ | async) || (isLoading$ | async)" + [disabled]=" + ((cartService.pendingTransaction$ | async) && + !helper.isExpired(cartService.pendingTransaction$ | async)) || + (isLoading$ | async) + " nz-tooltip [nzTooltipTitle]="(isLoading$ | async) ? 'Waiting for cart items to finish loading' : ''" class="text-red-600 hover:text-red-800" @@ -425,7 +429,6 @@ Checkout
-
diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.ts b/src/app/components/cart/components/cart-modal/cart-modal.component.ts index e78cc7d..ff69319 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.ts +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.ts @@ -1,4 +1,10 @@ -import { Component, OnDestroy, ChangeDetectorRef, ChangeDetectionStrategy, OnInit } from '@angular/core'; +import { + Component, + OnDestroy, + ChangeDetectorRef, + ChangeDetectionStrategy, + OnInit, +} from '@angular/core'; import { Network, Transaction } from '@build-5/interfaces'; import { Subscription, take, of, Observable, BehaviorSubject, Subject } from 'rxjs'; import { CartService, CartItem } from '@components/cart/services/cart.service'; @@ -9,7 +15,7 @@ import { UnitsService } from '@core/services/units/units.service'; import { map, switchMap, tap } from 'rxjs/operators'; import { DeviceService } from '@core/services/device'; import { HelperService } from '@pages/nft/services/helper.service'; -import { } from '@angular/core'; +import {} from '@angular/core'; export enum StepType { CONFIRM = 'Confirm', @@ -52,7 +58,7 @@ export class CartModalComponent implements OnInit, OnDestroy { public currentStep: StepType | null = null; public isTransactionExpired: boolean | null = null; public selectedNetwork: string | null = null; - public isLoading: boolean = false; + public isLoading = false; public pendingTransaction: Transaction | undefined = undefined; constructor( @@ -67,43 +73,43 @@ export class CartModalComponent implements OnInit, OnDestroy { ngOnInit() { this.subscriptions$.add( - this.cartService.currentStep$.subscribe(step => { - this.currentStep = step; - this.triggerChangeDetection(); - }) + this.cartService.currentStep$.subscribe((step) => { + this.currentStep = step; + this.triggerChangeDetection(); + }), ); this.subscriptions$.add( - this.cartService.selectedNetwork$.subscribe(network => { - this.selectedNetwork = network; - this.triggerChangeDetection(); - }) + this.cartService.selectedNetwork$.subscribe((network) => { + this.selectedNetwork = network; + this.triggerChangeDetection(); + }), ); this.subscriptions$.add( - this.cartService.pendingTransaction$.subscribe(transaction => { + this.cartService.pendingTransaction$.subscribe((transaction) => { this.pendingTransaction = transaction; this.triggerChangeDetection(); - }) + }), ); this.subscriptions$.add( - this.cartService.isLoading$.subscribe(loading => { - this.isLoading = loading; - this.triggerChangeDetection(); - }) + this.cartService.isLoading$.subscribe((loading) => { + this.isLoading = loading; + this.triggerChangeDetection(); + }), ); this.subscriptions$.add( - this.cartService.cartUpdateObservable$.subscribe(() => { - this.triggerChangeDetection(); - }) + this.cartService.cartUpdateObservable$.subscribe(() => { + this.triggerChangeDetection(); + }), ); this.subscriptions$.add( this.cartService.triggerChangeDetectionSubject$.subscribe(() => { - this.triggerChangeDetection(); - }) + this.triggerChangeDetection(); + }), ); } diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.html b/src/app/components/cart/components/checkout/checkout-overlay.component.html index a701645..b8932f6 100644 --- a/src/app/components/cart/components/checkout/checkout-overlay.component.html +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.html @@ -26,8 +26,8 @@

Network: {{ group.tokenSymbol }} - - {{ group.totalQuantity }} NFTs in network group - with total Price of + {{ group.totalQuantity }} NFTs in network group with + total Price of {{ group.totalPrice | formatToken : group.network : true : true | async }} ({{ unitsService.getUsd( cartService.valueDivideExponent({ diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts index 3124690..b3eaee4 100644 --- a/src/app/components/cart/services/cart.service.ts +++ b/src/app/components/cart/services/cart.service.ts @@ -121,39 +121,42 @@ export class CartService { private listenToStorageChanges(): void { window.addEventListener('storage', (event) => { - if (event.storageArea === localStorage) { - this.zone.run(() => { - switch (event.key) { - case CART_STORAGE_KEY: - const updatedCartItems = JSON.parse(event.newValue || '[]'); - this.cartItemsSubject$.next(updatedCartItems); - break; - case NETWORK_STORAGE_KEY: - const updatedNetwork = JSON.parse(event.newValue || 'null'); - this.selectedNetworkSubject$.next(updatedNetwork); - break; - case STEP_STORAGE_KEY: - const newStep = JSON.parse(event.newValue || 'null'); - this.currentStepSubject$.next(newStep); - break; - case TRAN_STORAGE_KEY: - const newTran = JSON.parse(event.newValue || 'null'); - this.pendingTransaction$.next(newTran); - break; - } - this.cartUpdateSubject$.next(); - }); - } + if (event.storageArea === localStorage) { + this.zone.run(() => { + switch (event.key) { + case CART_STORAGE_KEY: { + const updatedCartItems = JSON.parse(event.newValue || '[]'); + this.cartItemsSubject$.next(updatedCartItems); + break; + } + case NETWORK_STORAGE_KEY: { + const updatedNetwork = JSON.parse(event.newValue || 'null'); + this.selectedNetworkSubject$.next(updatedNetwork); + break; + } + case STEP_STORAGE_KEY: { + const newStep = JSON.parse(event.newValue || 'null'); + this.currentStepSubject$.next(newStep); + break; + } + case TRAN_STORAGE_KEY: { + const newTran = JSON.parse(event.newValue || 'null'); + this.pendingTransaction$.next(newTran); + break; + } + } + this.cartUpdateSubject$.next(); + }); + } }); } public startTransactionExpiryCheck(): void { if (this.transactionCheckInterval) { - clearInterval(this.transactionCheckInterval); + clearInterval(this.transactionCheckInterval); } this.transactionCheckInterval = setInterval(() => { - const transaction = this.pendingTransaction$.getValue(); const currentStep = this.getCurrentStep(); if (currentStep === StepType.TRANSACTION || currentStep === StepType.WAIT) { From 66a6781a0a2afdf2eaab28872dd4b1cd1d35976a Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Fri, 8 Mar 2024 17:19:29 -0500 Subject: [PATCH 22/24] Remove tester string from cart modal --- .../cart/components/cart-modal/cart-modal.component.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.html b/src/app/components/cart/components/cart-modal/cart-modal.component.html index 882b497..ecc1467 100644 --- a/src/app/components/cart/components/cart-modal/cart-modal.component.html +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.html @@ -383,11 +383,6 @@

Your cart is empty. -
- Pending Transaction: {{ cartService.pendingTransaction$ | async }} -
- Transaction Expired: {{ helper.isExpired(cartService.pendingTransaction$ | async) }} -
From bde71d4c76651bb71b91f2b4f5755c869578b57a Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Wed, 13 Mar 2024 00:35:17 -0400 Subject: [PATCH 23/24] Fixes for member meta data fetch issues bug --- .../member-about/member-about.component.ts | 4 +- .../member/pages/activity/activity.page.ts | 4 +- .../pages/member/pages/awards/awards.page.ts | 4 +- .../pages/member/pages/badges/badges.page.ts | 4 +- .../pages/member/pages/member/member.page.ts | 39 +++++++++++++++---- src/app/pages/member/pages/nfts/nfts.page.ts | 4 +- .../pages/spaces/member-spaces.component.ts | 4 +- .../pages/member/pages/tokens/tokens.page.ts | 4 +- .../pages/transactions/transactions.page.ts | 4 +- 9 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/app/components/member/components/member-about/member-about.component.ts b/src/app/components/member/components/member-about/member-about.component.ts index 15f2db9..6791b3c 100644 --- a/src/app/components/member/components/member-about/member-about.component.ts +++ b/src/app/components/member/components/member-about/member-about.component.ts @@ -63,7 +63,7 @@ export class MemberAboutComponent implements OnInit { this.drawerVisible$.next(false); } - public trackByUid(index: number, item: any): number { - return item.uid; + public trackByUid(index: number, item: any): any { + return item ? item.uid : index; } } diff --git a/src/app/pages/member/pages/activity/activity.page.ts b/src/app/pages/member/pages/activity/activity.page.ts index 2df3e41..11f97b2 100644 --- a/src/app/pages/member/pages/activity/activity.page.ts +++ b/src/app/pages/member/pages/activity/activity.page.ts @@ -184,7 +184,7 @@ export class ActivityPage implements OnInit { return ['/', ROUTER_UTILS.config.space.root, spaceId]; } - public trackByUid(index: number, item: any): number { - return item.uid; + public trackByUid(index: number, item: any): any { + return item ? item.uid : index; } } diff --git a/src/app/pages/member/pages/awards/awards.page.ts b/src/app/pages/member/pages/awards/awards.page.ts index a4c0e1d..3cb6b2a 100644 --- a/src/app/pages/member/pages/awards/awards.page.ts +++ b/src/app/pages/member/pages/awards/awards.page.ts @@ -50,7 +50,7 @@ export class AwardsPage { return this.auth.isLoggedIn$; } - public trackByUid(index: number, item: any): number { - return item.uid; + public trackByUid(index: number, item: any): any { + return item ? item.uid : index; } } diff --git a/src/app/pages/member/pages/badges/badges.page.ts b/src/app/pages/member/pages/badges/badges.page.ts index 73b7372..c10c07c 100644 --- a/src/app/pages/member/pages/badges/badges.page.ts +++ b/src/app/pages/member/pages/badges/badges.page.ts @@ -124,7 +124,7 @@ export class BadgesPage implements OnInit { return this.auth.isLoggedIn$; } - public trackByUid(index: number, item: Transaction) { - return item.uid; + public trackByUid(index: number, item: any): any { + return item ? item.uid : index; } } diff --git a/src/app/pages/member/pages/member/member.page.ts b/src/app/pages/member/pages/member/member.page.ts index 6043451..3a5994f 100644 --- a/src/app/pages/member/pages/member/member.page.ts +++ b/src/app/pages/member/pages/member/member.page.ts @@ -16,7 +16,7 @@ import { SeoService } from '@core/services/seo'; import { ROUTER_UTILS } from '@core/utils/router.utils'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { FILE_SIZES, Member } from '@build-5/interfaces'; -import { BehaviorSubject, Subscription, skip } from 'rxjs'; +import { BehaviorSubject, Subscription, filter, map, skip } from 'rxjs'; import { MemberApi } from './../../../../@api/member.api'; import { NavigationService } from './../../../../@core/services/navigation/navigation.service'; import { DataService } from './../../services/data.service'; @@ -104,6 +104,16 @@ export class MemberPage implements OnInit, OnDestroy { } public listenMember(memberId: string): void { + const isValidMemberId = typeof memberId === 'string' && /[a-zA-Z0-9]/.test(memberId); + + if (!isValidMemberId) { + this.data.awardsCompleted$.next([]); + this.data.awardsPending$.next([]); + this.data.space$.next([]); + this.data.member$.next(undefined); + return; + } + this.subscriptions$.push( this.memberApi .topAwardsCompleted(memberId) @@ -118,19 +128,34 @@ export class MemberPage implements OnInit, OnDestroy { ); // TODO Implement search. This is parked since we will be implementing new search here. this.subscriptions$.push( - this.memberApi.topSpaces(memberId).pipe(untilDestroyed(this)).subscribe(this.data.space$), + this.memberApi + .topSpaces(memberId) + .pipe( + untilDestroyed(this), + map((spaces) => + spaces.filter( + (space) => space && typeof space.uid === 'string' && space.uid.trim() !== '', + ), + ), + ) + .subscribe((filteredSpaces) => { + this.data.space$.next(filteredSpaces); + }), ); this.subscriptions$.push( this.memberApi .listen(memberId) - .pipe(untilDestroyed(this)) - .subscribe((v) => { - // Only pass next stage. - this.data.member$.next(v); + .pipe( + untilDestroyed(this), + filter((v) => v && typeof v.uid === 'string' && v.uid.trim() !== ''), + ) + .subscribe((validMember) => { + // Only pass next stage if valid. + this.data.member$.next(validMember); }), ); - // Badges. + // Continue with other actions if memberId is valid. this.data.refreshBadges(); } diff --git a/src/app/pages/member/pages/nfts/nfts.page.ts b/src/app/pages/member/pages/nfts/nfts.page.ts index 3d76ac3..9f07f2d 100644 --- a/src/app/pages/member/pages/nfts/nfts.page.ts +++ b/src/app/pages/member/pages/nfts/nfts.page.ts @@ -75,8 +75,8 @@ export class NFTsPage implements OnInit { return this.auth.member$; } - public trackByUid(_index: number, item: any): number { - return item.uid; + public trackByUid(index: number, item: any): any { + return item ? item.uid : index; } public convertAllToSoonaverseModel(algoliaItems: any[]) { diff --git a/src/app/pages/member/pages/spaces/member-spaces.component.ts b/src/app/pages/member/pages/spaces/member-spaces.component.ts index af34481..f383c53 100644 --- a/src/app/pages/member/pages/spaces/member-spaces.component.ts +++ b/src/app/pages/member/pages/spaces/member-spaces.component.ts @@ -49,7 +49,7 @@ export class MemberSpacesComponent implements OnInit { ); } - public trackByUid(index: number, item: any): number { - return item.uid; + public trackByUid(index: number, item: any): any { + return item ? item.uid : index; } } diff --git a/src/app/pages/member/pages/tokens/tokens.page.ts b/src/app/pages/member/pages/tokens/tokens.page.ts index 6a13fe8..98e1d25 100644 --- a/src/app/pages/member/pages/tokens/tokens.page.ts +++ b/src/app/pages/member/pages/tokens/tokens.page.ts @@ -289,8 +289,8 @@ export class TokensPage implements OnInit, OnDestroy { ); } - public trackByUid(index: number, item: any): number { - return item.uid; + public trackByUid(index: number, item: any): any { + return item ? item.uid : index; } public understandNotMintedWarning(): void { diff --git a/src/app/pages/member/pages/transactions/transactions.page.ts b/src/app/pages/member/pages/transactions/transactions.page.ts index dd547e2..9107dd6 100644 --- a/src/app/pages/member/pages/transactions/transactions.page.ts +++ b/src/app/pages/member/pages/transactions/transactions.page.ts @@ -201,8 +201,8 @@ export class TransactionsPage implements OnInit, OnDestroy { }); } - public trackByUid(index: number, item: any): number { - return item.uid; + public trackByUid(index: number, item: any): any { + return item ? item.uid : index; } private cancelSubscriptions(): void { From fba8fddec320e1c31bd26e8153ddb03f4990302c Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 14 Mar 2024 10:51:49 -0700 Subject: [PATCH 24/24] added contributions --- CONTRIBUTORS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b9beb43..41e1ba3 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -6,3 +6,5 @@ User | PR | SOON Reward | SMR Address | Authorized By | Comments | ---- | -- | ----------- | ----------- | ------------- | -------- | [@emmap3-do](https://github.com/emmap3-do) | https://github.com/soonaverse/app/pull/47 | 500 | smr1qzt5qs6m6s2us8ll0hdfefzpr43cdz2xmjzywmrkz0sc2uyegvzjwazr6f8 | [@adam_unchained](https://github.com/adam_unchained) | Testing, continuous support in #dev channel [@emmap3-do](https://github.com/emmap3-do) | https://github.com/soonaverse/app/pull/56 | 75 | smr1qzt5qs6m6s2us8ll0hdfefzpr43cdz2xmjzywmrkz0sc2uyegvzjwazr6f8 | [@adam_unchained](https://github.com/adam_unchained) | Minor fixes +[@amenconi](https://github.com/amenconi) | https://github.com/soonaverse/app/pull/128 | 76'000 | smr1qrncyy5lcfpr4hta0hg7qp2cmw6ssm0ycllx5nnz5pwcup8rxs0zzp2jp64 | [SOON_COMMITTEE - request 83](https://github.com/soonaverse/foundation/issues/83) | Bulk Buying feature +[@emmap3-do](https://github.com/emmap3-do) | https://github.com/soonaverse/app/pull/128 | 15'200 | smr1qzt5qs6m6s2us8ll0hdfefzpr43cdz2xmjzywmrkz0sc2uyegvzjwazr6f8 | [SOON_COMMITTEE - request 83](https://github.com/soonaverse/foundation/issues/83) | Bulk Buying feature