Skip to content
This repository has been archived by the owner on Oct 4, 2023. It is now read-only.

Commit

Permalink
[PAY-1715] Adds Sales page (#3957)
Browse files Browse the repository at this point in the history
  • Loading branch information
schottra authored Aug 29, 2023
1 parent 7d2f718 commit 68bba0d
Show file tree
Hide file tree
Showing 36 changed files with 803 additions and 243 deletions.
6 changes: 3 additions & 3 deletions packages/common/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"url": "https://github.com/AudiusProject/audius-client/issues"
},
"dependencies": {
"@audius/sdk": "3.0.8-beta.1",
"@audius/sdk": "3.0.8-beta.3",
"@fingerprintjs/fingerprintjs-pro": "3.5.6",
"@metaplex-foundation/mpl-token-metadata": "2.5.2",
"@optimizely/optimizely-sdk": "4.0.0",
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './developerApps'
export * from './suggestedTracks'
export * from './trending'
export * from './library'
export * from './purchases'
165 changes: 165 additions & 0 deletions packages/common/src/api/purchases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { full } from '@audius/sdk'

import { createApi } from 'audius-query'
import { ID } from 'models'
import {
USDCContentPurchaseType,
USDCPurchaseDetails
} from 'models/USDCTransactions'
import { StringUSDC } from 'models/Wallet'
import { Nullable } from 'utils/typeUtils'

import { trackApiFetch } from './track'
import { HashId, Id } from './utils'

type GetPurchaseListArgs = {
userId: Nullable<ID>
offset: number
limit: number
sortMethod?: full.GetPurchasesSortMethodEnum
sortDirection?: full.GetPurchasesSortDirectionEnum
}

const parsePurchase = (purchase: full.Purchase): USDCPurchaseDetails => {
const { contentId, contentType, amount, buyerUserId, sellerUserId, ...rest } =
purchase
return {
...rest,
contentType: contentType as USDCContentPurchaseType,
contentId: HashId.parse(contentId),
amount: amount as StringUSDC,
buyerUserId: HashId.parse(buyerUserId),
sellerUserId: HashId.parse(sellerUserId)
}
}

const purchasesApi = createApi({
reducerPath: 'purchasesApi',
endpoints: {
getPurchases: {
fetch: async (
{
offset,
limit,
userId,
sortDirection,
sortMethod
}: GetPurchaseListArgs,
context
) => {
const { data: encodedDataMessage, signature: encodedDataSignature } =
await context.audiusBackend.signDiscoveryNodeRequest()
const sdk = await context.audiusSdk()
const { data = [] } = await sdk.full.users.getPurchases({
limit,
offset,
sortDirection,
sortMethod,
id: Id.parse(userId!),
userId: Id.parse(userId!),
encodedDataMessage,
encodedDataSignature
})
const purchases = data.map(parsePurchase)

// Pre-fetch track metadata
const trackIdsToFetch = purchases
.filter(
({ contentType }) => contentType === USDCContentPurchaseType.TRACK
)
.map(({ contentId }) => contentId)
await trackApiFetch.getTracksByIds(
{ ids: trackIdsToFetch, currentUserId: userId },
context
)
return purchases
},
options: {}
},
getPurchasesCount: {
fetch: async (
{ userId }: Pick<GetPurchaseListArgs, 'userId'>,
{ audiusSdk, audiusBackend }
) => {
const { data: encodedDataMessage, signature: encodedDataSignature } =
await audiusBackend.signDiscoveryNodeRequest()
const sdk = await audiusSdk()
const { data } = await sdk.full.users.getPurchasesCount({
id: Id.parse(userId!),
userId: Id.parse(userId!),
encodedDataMessage,
encodedDataSignature
})
return data ?? 0
},
options: {}
},
getSales: {
fetch: async (
{
offset,
limit,
userId,
sortDirection,
sortMethod
}: GetPurchaseListArgs,
context
) => {
const { data: encodedDataMessage, signature: encodedDataSignature } =
await context.audiusBackend.signDiscoveryNodeRequest()
const sdk = await context.audiusSdk()
const { data = [] } = await sdk.full.users.getSales({
limit,
offset,
sortDirection,
sortMethod,
id: Id.parse(userId!),
userId: Id.parse(userId!),
encodedDataMessage,
encodedDataSignature
})

const purchases = data.map(parsePurchase)

// Pre-fetch track metadata
const trackIdsToFetch = purchases
.filter(
({ contentType }) => contentType === USDCContentPurchaseType.TRACK
)
.map(({ contentId }) => contentId)
await trackApiFetch.getTracksByIds(
{ ids: trackIdsToFetch, currentUserId: userId },
context
)
return purchases
},
options: {}
},
getSalesCount: {
fetch: async (
{ userId }: Pick<GetPurchaseListArgs, 'userId'>,
{ audiusSdk, audiusBackend }
) => {
const { data: encodedDataMessage, signature: encodedDataSignature } =
await audiusBackend.signDiscoveryNodeRequest()
const sdk = await audiusSdk()
const { data } = await sdk.full.users.getSalesCount({
id: Id.parse(userId!),
userId: Id.parse(userId!),
encodedDataMessage,
encodedDataSignature
})
return data ?? 0
},
options: {}
}
}
})

export const {
useGetPurchases,
useGetPurchasesCount,
useGetSales,
useGetSalesCount
} = purchasesApi.hooks
export const purchasesApiReducer = purchasesApi.reducer
4 changes: 3 additions & 1 deletion packages/common/src/api/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { collectionApiReducer } from './collection'
import { developerAppsApiReducer } from './developerApps'
import { favoritesApiReducer } from './favorites'
import { libraryApiReducer } from './library'
import { purchasesApiReducer } from './purchases'
import { relatedArtistsApiReducer } from './relatedArtists'
import { trackApiReducer } from './track'
import { trendingApiReducer } from './trending'
Expand All @@ -17,5 +18,6 @@ export default combineReducers({
developerAppsApi: developerAppsApiReducer,
favoritesApi: favoritesApiReducer,
trendingApi: trendingApiReducer,
libraryApi: libraryApiReducer
libraryApi: libraryApiReducer,
purchasesApi: purchasesApiReducer
})
1 change: 1 addition & 0 deletions packages/common/src/api/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,5 @@ const trackApi = createApi({

export const { useGetTrackById, useGetTrackByPermalink, useGetTracksByIds } =
trackApi.hooks
export const trackApiFetch = trackApi.fetch
export const trackApiReducer = trackApi.reducer
54 changes: 1 addition & 53 deletions packages/common/src/api/user.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,5 @@
import { full } from '@audius/sdk'

import { createApi } from 'audius-query'
import { ID, Kind } from 'models'
import {
USDCContentPurchaseType,
USDCPurchaseDetails
} from 'models/USDCTransactions'
import { StringUSDC } from 'models/Wallet'
import { encodeHashId } from 'utils/hashIds'
import { Nullable } from 'utils/typeUtils'

type GetPurchasesArgs = {
userId: Nullable<ID>
offset: number
limit: number
sortMethod?: full.GetPurchasesSortMethodEnum
sortDirection?: full.GetPurchasesSortDirectionEnum
}

const parsePurchase = (purchase: full.Purchase): USDCPurchaseDetails => {
const { contentId, contentType, amount, buyerUserId, sellerUserId, ...rest } =
purchase
return {
...rest,
contentType: contentType as USDCContentPurchaseType,
contentId: Number.parseInt(contentId),
amount: amount as StringUSDC,
buyerUserId: Number.parseInt(buyerUserId),
sellerUserId: Number.parseInt(sellerUserId)
}
}

const userApi = createApi({
reducerPath: 'userApi',
Expand All @@ -47,31 +17,9 @@ const userApi = createApi({
kind: Kind.USERS,
schemaKey: 'user'
}
},
getPurchases: {
fetch: async (
{ offset, limit, userId, sortDirection, sortMethod }: GetPurchasesArgs,
{ audiusSdk, audiusBackend }
) => {
const { data: encodedDataMessage, signature: encodedDataSignature } =
await audiusBackend.signDiscoveryNodeRequest()
const sdk = await audiusSdk()
const { data: purchases = [] } = await sdk.full.users.getPurchases({
limit,
offset,
sortDirection,
sortMethod,
id: encodeHashId(userId!),
userId: encodeHashId(userId!),
encodedDataMessage,
encodedDataSignature
})
return purchases.map(parsePurchase)
},
options: {}
}
}
})

export const { useGetUserById, useGetPurchases } = userApi.hooks
export const { useGetUserById } = userApi.hooks
export const userApiReducer = userApi.reducer
2 changes: 2 additions & 0 deletions packages/common/src/audius-query/AudiusQueryContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createContext } from 'react'

import { AudiusSdk } from '@audius/sdk'
import { Dispatch } from 'redux'

import type { AudiusAPIClient } from 'services/audius-api-client'
import { AudiusBackend } from 'services/index'
Expand All @@ -11,6 +12,7 @@ export type AudiusQueryContextType = {
apiClient: AudiusAPIClient
audiusSdk: () => Promise<AudiusSdk>
audiusBackend: AudiusBackend
dispatch: Dispatch
reportToSentry: (args: ReportToSentryArgs) => void
}

Expand Down
57 changes: 55 additions & 2 deletions packages/common/src/audius-query/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,11 @@
```
1. Export hooks (same process as query endpoints)

1. Use hook in your component
1. Use hook in your component
```typescript
const [updateSomeData, result] = useUpdateSomeData()
const { data: someData, status, errorMessage } = result
return (
<Button text="Update some data" onClick={updateSomeData} />
Expand Down Expand Up @@ -208,6 +208,59 @@ const api = createApi({
```


## Pre-fetching related entities

Many endpoints return related data as nested fields (ex. Tracks including a user field with basic info about a user). However, some endpoints only return identifiers pointing to the related entities. In cases where we know we will need that information before rendering, there are a couple of options.

### Cascading hooks

The simpler and more common usage is to use the output of one hook as the input for another:

```typescript
const {data: firstDataset, status: firstDatasetStatus} = useFirstDataset()
const {data: secondDataset, status: secondDatasetStatus} = useSecondDataset(firstDataset, {
disabled: firstDatasetStatus !== Status.Success
})
return secondDatasetStatus === Status.Success ? (
<SomeMultiDatasetComponent firstSet={firstDataset} secondSet={secondDataset} />
) : null
```

### Pre-fetching in endpoint implementations

The cascading hooks approach works well if you are loading a set of information, mapping it with another fetch, then
rendering the data. However, it falls apart a bit if you are working with _paginated data_. Fetching the first page and
mapping it is the same as before. But when fetching subsequent pages, we need some pretty messy logic to defer adding the
new page of data until its related entities have been fetched.

API definitions can opt in to exposing raw fetch functions by exporting the `api.fetch` property:
```typescript
const tracksApi = createApi(...)
export const tracksFetchApi = tracksApi.fetch
```

The raw fetch implementations are designed to be called from other API endpoint definitions and will require passing in the `AudiusQueryContext`.

The following approach works well for fetching pages of data, then pre-fetching related data that will be queried via other api hooks in child components. For example, the Purchases table fetches a list of records that include content IDs, and then each individual row will attempt to look up the content by ID using the appropriate useGetTrackById() hook. Since the data has already been pre-fetched, the hooks in child components should hit cached values. If they happen to miss, they will go ahead and fetch.

```typescript
import {tracksFetchApi} from './tracksApi'
const purchasesApi = createApi({
endpoints:
getPurchases: async (args, context, options) => {
//... fetch purchases
const trackIdsToFetch = purchases.map(extractTrackIds)
await tracksFetchApi.getTracksByIds({ids: trackIdsToFetch}, context)
return purchases
}
})
```

Note that in the above code, we're simply issuing a fetch for the related tracks, which will be inserted into the cache, before returning our original data. This is merely a performance optimization.


## Query Hook options
Expand Down
Loading

0 comments on commit 68bba0d

Please sign in to comment.