Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: prevent user from performing List in CaptureClub with the same a… #1411

Merged
merged 1 commit into from
Mar 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 78 additions & 6 deletions src/app/features/home/details/actions/actions.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute } from '@angular/router';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { combineLatest, of } from 'rxjs';
import { catchError, concatMap, first, map, tap } from 'rxjs/operators';
import { combineLatest, forkJoin, of } from 'rxjs';
import { catchError, concatMap, first, map, take, tap } from 'rxjs/operators';
import { ActionsDialogComponent } from '../../../../shared/actions/actions-dialog/actions-dialog.component';
import {
Action,
Expand All @@ -14,13 +14,17 @@ import {
import { OrderHistoryService } from '../../../../shared/actions/service/order-history.service';
import { BlockingActionService } from '../../../../shared/blocking-action/blocking-action.service';
import { DiaBackendAuthService } from '../../../../shared/dia-backend/auth/dia-backend-auth.service';
import { DiaBackendSeriesRepository } from '../../../../shared/dia-backend/series/dia-backend-series-repository.service';
import {
DiaBackendStoreService,
NetworkAppOrder,
} from '../../../../shared/dia-backend/store/dia-backend-store.service';
import { ErrorService } from '../../../../shared/error/error.service';
import { OrderDetailDialogComponent } from '../../../../shared/order-detail-dialog/order-detail-dialog.component';
import { isNonNullable } from '../../../../utils/rx-operators/rx-operators';
import {
isNonNullable,
VOID$,
} from '../../../../utils/rx-operators/rx-operators';

@UntilDestroy()
@Component({
Expand All @@ -34,7 +38,8 @@ export class ActionsPage {
.pipe(catchError((err: unknown) => this.errorService.toastError$(err)));

private readonly id$ = this.route.paramMap.pipe(
map(params => params.get('id'))
map(params => params.get('id')),
isNonNullable()
);

constructor(
Expand All @@ -47,9 +52,71 @@ export class ActionsPage {
private readonly snackBar: MatSnackBar,
private readonly dialog: MatDialog,
private readonly storeService: DiaBackendStoreService,
private readonly orderHistoryService: OrderHistoryService
private readonly orderHistoryService: OrderHistoryService,
private readonly diaBackendStoreService: DiaBackendStoreService,
private readonly diaBackendSeriesRepository: DiaBackendSeriesRepository
) {}

canPerformAction$(action: Action) {
if (action.title_text === 'List in CaptureClub') {
/*
Workaround:
Currently there isn't a simple way to check whether an asset is listed in
CaptureClub or not. So I first query List all Products API with
associated_id parameter set to the assets cid. And then use list series
API and check through all nested collections. See discussion here
https://app.asana.com/0/0/1201558520076805/1201995911008176/f
*/
return this.id$.pipe(
concatMap(cid =>
forkJoin([
this.diaBackendStoreService.listAllProducts$({
associated_id: cid,
service_name: 'CaptureClub',
}),
of(cid),
])
),
concatMap(([response, cid]) => {
if (response.count > 0) {
throw new Error(
this.translocoService.translate('message.hasListedInCaptureClub')
);
}
return of(cid);
}),
concatMap(async cid => {
let currentOffset = 0;
const limit = 100;
while (true) {
const response = await this.diaBackendSeriesRepository
.fetchAll$({ offset: currentOffset, limit })
.toPromise();
const listedAsSeries = response.results.some(serie =>
serie.collections.some(collection =>
collection.assets.some(asset => asset.cid === cid)
)
);
if (listedAsSeries) {
throw new Error(
this.translocoService.translate(
'message.hasListedInCaptureClub'
)
);
}
if (response.next == null) {
break;
}
currentOffset += response.results.length;
}
return VOID$;
}),
take(1)
);
}
return VOID$;
}

openActionDialog$(action: Action) {
return combineLatest([
this.actionsService.getParams$(action.params_list_custom_param1),
Expand Down Expand Up @@ -128,8 +195,13 @@ export class ActionsPage {
}

doAction(action: Action) {
this.openActionDialog$(action)
this.blockingActionService
.run$(this.canPerformAction$(action))
.pipe(
catchError((err: unknown) => {
return this.errorService.toastError$(err);
}),
concatMap(() => this.openActionDialog$(action)),
concatMap(createOrderInput =>
this.blockingActionService.run$(
this.createOrder$(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { BASE_URL } from '../secret';
providedIn: 'root',
})
export class DiaBackendSeriesRepository {
private readonly fetchCollectedSeriesCount$ = this.fetch$({
private readonly fetchCollectedSeriesCount$ = this.fetchAll$({
limit: 1,
collected: true,
}).pipe(pluck('count'));

readonly fetchCollectedSeriesList$ = this.fetchCollectedSeriesCount$.pipe(
switchMap(count => this.fetch$({ limit: count, collected: true }))
switchMap(count => this.fetchAll$({ limit: count, collected: true }))
);

constructor(
Expand All @@ -28,23 +28,16 @@ export class DiaBackendSeriesRepository {
return this.read$(id);
}

private fetch$({
limit,
collected,
}: {
limit?: number;
collected?: boolean;
}) {
fetchAll$(
queryParams: {
limit?: number;
collected?: boolean;
offset?: number;
} = {}
) {
return defer(() => this.authService.getAuthHeaders()).pipe(
concatMap(headers => {
let params = new HttpParams();

if (limit !== undefined) {
params = params.set('limit', `${limit}`);
}
if (collected !== undefined) {
params = params.set('collected', `${collected}`);
}
const params = new HttpParams({ fromObject: queryParams });
return this.httpClient.get<PaginatedResponse<DiaBackendSeries>>(
`${BASE_URL}/api/v3/series/`,
{ headers, params }
Expand All @@ -70,6 +63,7 @@ export interface DiaBackendSeries {
readonly collections: {
readonly assets: {
readonly id: string;
readonly cid: string;
readonly asset_file_thumbnail: string;
readonly collected: boolean;
}[];
Expand Down
48 changes: 46 additions & 2 deletions src/app/shared/dia-backend/store/dia-backend-store.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { defer } from 'rxjs';
import { concatMap } from 'rxjs/operators';
import { DiaBackendAuthService } from '../auth/dia-backend-auth.service';
import { PaginatedResponse } from '../pagination';
import { BASE_URL } from '../secret';

@Injectable({
Expand Down Expand Up @@ -41,11 +42,25 @@ export class DiaBackendStoreService {
})
);
}

listAllProducts$(
queryParams: { associated_id?: string; service_name?: string } = {}
) {
return defer(() => this.authService.getAuthHeadersWithApiKey()).pipe(
concatMap(headers => {
const params = new HttpParams({ fromObject: queryParams });
return this.httpClient.get<PaginatedResponse<Product>>(
`${BASE_URL}/api/v3/store/products/`,
{ headers, params }
);
})
);
}
}

export interface NetworkAppOrder {
id: string;
status: 'completed' | 'canceled' | 'pending';
status: 'created' | 'success' | 'failure' | 'pending';
network_app_id: string;
network_app_name: string;
action: string;
Expand All @@ -63,3 +78,32 @@ export interface NetworkAppOrder {
created_at: string;
expired_at: string;
}

export interface Product {
id: string;
associated_id: string;
type: string;
service_name: string;
subject: string;
description: string;
tags: string[];
cover_image: string;
cover_image_thumbnail: string;
product_url: string;
external_product_url: string;
price: string;
price_base: 'usd' | 'num';
quantity: number;
in_stock: number;
nft_token_id: number;
nft_token_uri: string;
nft_blockchain_name: string;
nft_contract_address: string;
owner: string;
owner_name: string;
original_creator: string;
original_creator_name: string;
enabled: boolean;
created_at: string;
updated_at: string;
}
3 changes: 2 additions & 1 deletion src/assets/i18n/en-us.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@
"mintNftToken": "Mint NFT token",
"mintNftAlert": "Once the NFT is minted, photo and its information can be accessed publicly. Do you still want to proceed?",
"sentSuccessfully": "Sent Successfully",
"confirmCopyPrivateKey": "Warning: Please do not expose your private key to anyone. Lost of private key may cause your account to be lost permanently."
"confirmCopyPrivateKey": "Warning: Please do not expose your private key to anyone. Lost of private key may cause your account to be lost permanently.",
"hasListedInCaptureClub": "This asset has already been listed in CaptureClub."
},
"error": {
"validationError": "Invalid operation. Please check your input again.",
Expand Down
3 changes: 2 additions & 1 deletion src/assets/i18n/zh-tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@
"mintNftToken": "鑄造 NFT 代幣",
"mintNftAlert": "NFT 代幣鑄造後,影像以及所有資訊都將可被公開檢視。確定要繼續嗎?",
"sentSuccessfully": "成功送出",
"confirmCopyPrivateKey": "警告:請不要向任何人揭露您的金鑰,未保管好金鑰可能導致您永遠失去您的帳號。"
"confirmCopyPrivateKey": "警告:請不要向任何人揭露您的金鑰,未保管好金鑰可能導致您永遠失去您的帳號。",
"hasListedInCaptureClub": "該資產已在 CaptureClub 上架。"
},
"error": {
"validationError": "操作或輸入有誤,請確認您的操作正確。",
Expand Down