diff --git a/api_ui/api.yaml b/api_ui/api.yaml index 6d7acaf36..852dcaf12 100644 --- a/api_ui/api.yaml +++ b/api_ui/api.yaml @@ -881,16 +881,6 @@ paths: summary: Create Authentication Link QRCode operationId: CreateLinkQrCode parameters: - - name: type - in: query - required: false - schema: - type: string - enum: [ raw, link ] - description: > - Type: - * `link` - (default value) Return a QR code with a link redirection to the raw content. Easier to scan. - * `raw` - Return the raw QR code. (default value) - $ref: '#/components/parameters/id' tags: - Links @@ -1154,19 +1144,21 @@ components: type: string example: https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld#KYCAgeCredential - CredentialLinkQrCodeResponse: type: object required: - issuer - - qrCode + - qrCodeLink + - qrCodeRaw - sessionID - linkID - linkDetail properties: issuer: $ref: '#/components/schemas/IssuerDescription' - qrCode: + qrCodeRaw: + type: string + qrCodeLink: type: string example: iden3comm://?request_uri=https%3A%2F%2Fissuer-demo.polygonid.me%2Fapi%2Fqr-store%3Fid%3Df780a169-8959-4380-9461-f7200e2ed3f4 sessionID: diff --git a/internal/api_ui/api.gen.go b/internal/api_ui/api.gen.go index cff370cce..eb690b432 100644 --- a/internal/api_ui/api.gen.go +++ b/internal/api_ui/api.gen.go @@ -84,12 +84,6 @@ const ( GetLinksParamsStatusInactive GetLinksParamsStatus = "inactive" ) -// Defines values for CreateLinkQrCodeParamsType. -const ( - CreateLinkQrCodeParamsTypeLink CreateLinkQrCodeParamsType = "link" - CreateLinkQrCodeParamsTypeRaw CreateLinkQrCodeParamsType = "raw" -) - // Defines values for GetCredentialQrCodeParamsType. const ( GetCredentialQrCodeParamsTypeLink GetCredentialQrCodeParamsType = "link" @@ -170,7 +164,8 @@ type Credential struct { type CredentialLinkQrCodeResponse struct { Issuer IssuerDescription `json:"issuer"` LinkDetail LinkSimple `json:"linkDetail"` - QrCode string `json:"qrCode"` + QrCodeLink string `json:"qrCodeLink"` + QrCodeRaw string `json:"qrCodeRaw"` SessionID string `json:"sessionID"` } @@ -519,17 +514,6 @@ type GetLinkQRCodeParams struct { SessionID SessionID `form:"sessionID" json:"sessionID"` } -// CreateLinkQrCodeParams defines parameters for CreateLinkQrCode. -type CreateLinkQrCodeParams struct { - // Type Type: - // * `link` - (default value) Return a QR code with a link redirection to the raw content. Easier to scan. - // * `raw` - Return the raw QR code. (default value) - Type *CreateLinkQrCodeParamsType `form:"type,omitempty" json:"type,omitempty"` -} - -// CreateLinkQrCodeParamsType defines parameters for CreateLinkQrCode. -type CreateLinkQrCodeParamsType string - // GetCredentialQrCodeParams defines parameters for GetCredentialQrCode. type GetCredentialQrCodeParams struct { // Type Type: @@ -646,7 +630,7 @@ type ServerInterface interface { GetLinkQRCode(w http.ResponseWriter, r *http.Request, id Id, params GetLinkQRCodeParams) // Create Authentication Link QRCode // (POST /v1/credentials/links/{id}/qrcode) - CreateLinkQrCode(w http.ResponseWriter, r *http.Request, id Id, params CreateLinkQrCodeParams) + CreateLinkQrCode(w http.ResponseWriter, r *http.Request, id Id) // Get Revocation Status // (GET /v1/credentials/revocation/status/{nonce}) GetRevocationStatus(w http.ResponseWriter, r *http.Request, nonce PathNonce) @@ -832,7 +816,7 @@ func (_ Unimplemented) GetLinkQRCode(w http.ResponseWriter, r *http.Request, id // Create Authentication Link QRCode // (POST /v1/credentials/links/{id}/qrcode) -func (_ Unimplemented) CreateLinkQrCode(w http.ResponseWriter, r *http.Request, id Id, params CreateLinkQrCodeParams) { +func (_ Unimplemented) CreateLinkQrCode(w http.ResponseWriter, r *http.Request, id Id) { w.WriteHeader(http.StatusNotImplemented) } @@ -1634,19 +1618,8 @@ func (siw *ServerInterfaceWrapper) CreateLinkQrCode(w http.ResponseWriter, r *ht return } - // Parameter object where we will unmarshal all parameters from the context - var params CreateLinkQrCodeParams - - // ------------- Optional query parameter "type" ------------- - - err = runtime.BindQueryParameter("form", true, false, "type", r.URL.Query(), ¶ms.Type) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "type", Err: err}) - return - } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.CreateLinkQrCode(w, r, id, params) + siw.Handler.CreateLinkQrCode(w, r, id) })) for _, middleware := range siw.HandlerMiddlewares { @@ -2986,8 +2959,7 @@ func (response GetLinkQRCode500JSONResponse) VisitGetLinkQRCodeResponse(w http.R } type CreateLinkQrCodeRequestObject struct { - Id Id `json:"id"` - Params CreateLinkQrCodeParams + Id Id `json:"id"` } type CreateLinkQrCodeResponseObject interface { @@ -4283,11 +4255,10 @@ func (sh *strictHandler) GetLinkQRCode(w http.ResponseWriter, r *http.Request, i } // CreateLinkQrCode operation middleware -func (sh *strictHandler) CreateLinkQrCode(w http.ResponseWriter, r *http.Request, id Id, params CreateLinkQrCodeParams) { +func (sh *strictHandler) CreateLinkQrCode(w http.ResponseWriter, r *http.Request, id Id) { var request CreateLinkQrCodeRequestObject request.Id = id - request.Params = params handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.CreateLinkQrCode(ctx, request.(CreateLinkQrCodeRequestObject)) diff --git a/internal/api_ui/responses.go b/internal/api_ui/responses.go index bbe2f8a1e..9effcee01 100644 --- a/internal/api_ui/responses.go +++ b/internal/api_ui/responses.go @@ -164,7 +164,7 @@ func connectionsResponse(conns []*domain.Connection) (GetConnectionsResponse, er return resp, nil } -func connectionsPaginatedResponse(conns []*domain.Connection, pagFilter *pagination.Filter, total uint) (ConnectionsPaginated, error) { +func connectionsPaginatedResponse(conns []*domain.Connection, pagFilter pagination.Filter, total uint) (ConnectionsPaginated, error) { resp, err := connectionsResponse(conns) if err != nil { return ConnectionsPaginated{}, err @@ -173,13 +173,13 @@ func connectionsPaginatedResponse(conns []*domain.Connection, pagFilter *paginat connsPag := ConnectionsPaginated{ Items: resp, Meta: PaginatedMetadata{ - Page: 1, // default - Total: total, + MaxResults: pagFilter.MaxResults, + Page: 1, // default + Total: total, }, } - if pagFilter != nil { + if pagFilter.Page != nil { connsPag.Meta.Page = *pagFilter.Page - connsPag.Meta.MaxResults = pagFilter.MaxResults } return connsPag, nil diff --git a/internal/api_ui/server.go b/internal/api_ui/server.go index b62cf44e3..072df6528 100644 --- a/internal/api_ui/server.go +++ b/internal/api_ui/server.go @@ -595,22 +595,20 @@ func (s *Server) CreateLinkQrCode(ctx context.Context, req CreateLinkQrCodeReque return CreateLinkQrCode500JSONResponse{N500JSONResponse{"Unexpected error while creating qr code"}}, nil } - qrContent := createLinkQrCodeResponse.QrCode // Backward compatibility. If the type is raw, we return the raw qr code - if req.Params.Type != nil && *req.Params.Type == CreateLinkQrCodeParamsTypeRaw { - rawQrCode, err := s.qrService.Find(ctx, createLinkQrCodeResponse.QrID) - if err != nil { - log.Error(ctx, "qr store. Finding qr", "err", err, "id", createLinkQrCodeResponse.QrID) - return CreateLinkQrCode500JSONResponse{N500JSONResponse{"error looking for qr body"}}, nil - } - qrContent = string(rawQrCode) + qrCodeRaw, err := s.qrService.Find(ctx, createLinkQrCodeResponse.QrID) + if err != nil { + log.Error(ctx, "qr store. Finding qr", "err", err, "id", createLinkQrCodeResponse.QrID) + return CreateLinkQrCode500JSONResponse{N500JSONResponse{"error looking for qr body"}}, nil } + return CreateLinkQrCode200JSONResponse{ Issuer: IssuerDescription{ DisplayName: s.cfg.APIUI.IssuerName, Logo: s.cfg.APIUI.IssuerLogo, }, - QrCode: qrContent, + QrCodeLink: createLinkQrCodeResponse.QrCode, + QrCodeRaw: string(qrCodeRaw), SessionID: createLinkQrCodeResponse.SessionID, LinkDetail: getLinkSimpleResponse(*createLinkQrCodeResponse.Link), }, nil diff --git a/internal/api_ui/server_test.go b/internal/api_ui/server_test.go index 9b197605b..3243206e4 100644 --- a/internal/api_ui/server_test.go +++ b/internal/api_ui/server_test.go @@ -2904,7 +2904,7 @@ func TestServer_GetConnections(t *testing.T) { auth: authOk, request: GetConnectionsRequestObject{ Params: GetConnectionsParams{ - MaxResults: common.ToPointer(uint(1)), + MaxResults: common.ToPointer(uint(2)), }, }, expected: expected{ @@ -4298,7 +4298,6 @@ func TestServer_CreateLinkQRCode(t *testing.T) { type expected struct { linkDetail Link httpCode int - qrWithLink bool message string } @@ -4320,8 +4319,7 @@ func TestServer_CreateLinkQRCode(t *testing.T) { { name: "Expired link", request: CreateLinkQrCodeRequestObject{ - Id: linkExpired.ID, - Params: CreateLinkQrCodeParams{Type: nil}, + Id: linkExpired.ID, }, expected: expected{ httpCode: http.StatusNotFound, @@ -4329,38 +4327,12 @@ func TestServer_CreateLinkQRCode(t *testing.T) { }, }, { - name: "Happy path without qr type, expecting a qr code with link", - request: CreateLinkQrCodeRequestObject{ - Id: link.ID, - Params: CreateLinkQrCodeParams{Type: nil}, - }, - expected: expected{ - linkDetail: linkDetail, - qrWithLink: true, - httpCode: http.StatusOK, - }, - }, - { - name: "Happy path with qr type == link", - request: CreateLinkQrCodeRequestObject{ - Id: link.ID, - Params: CreateLinkQrCodeParams{Type: common.ToPointer(CreateLinkQrCodeParamsTypeLink)}, - }, - expected: expected{ - linkDetail: linkDetail, - qrWithLink: true, - httpCode: http.StatusOK, - }, - }, - { - name: "Happy path with qr type == raw", + name: "Happy path", request: CreateLinkQrCodeRequestObject{ - Id: link.ID, - Params: CreateLinkQrCodeParams{Type: common.ToPointer(CreateLinkQrCodeParamsTypeRaw)}, + Id: link.ID, }, expected: expected{ linkDetail: linkDetail, - qrWithLink: false, httpCode: http.StatusOK, }, }, @@ -4368,9 +4340,6 @@ func TestServer_CreateLinkQRCode(t *testing.T) { t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() apiURL := fmt.Sprintf("/v1/credentials/links/%s/qrcode", tc.request.Id.String()) - if tc.request.Params.Type != nil { - apiURL = apiURL + "?type=" + string(*tc.request.Params.Type) - } req, err := http.NewRequest(http.MethodPost, apiURL, tests.JSONBody(t, nil)) require.NoError(t, err) @@ -4386,20 +4355,17 @@ func TestServer_CreateLinkQRCode(t *testing.T) { require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response)) realQR := protocol.AuthorizationRequestMessage{} - if tc.expected.qrWithLink { - qrLink := checkQRfetchURL(t, response.QrCode) - // Now let's fetch the original QR using the url - rr := httptest.NewRecorder() - req, err := http.NewRequest(http.MethodGet, qrLink, nil) - require.NoError(t, err) - handler.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) + qrLink := checkQRfetchURL(t, response.QrCodeLink) - require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &realQR)) - } else { - require.NoError(t, json.Unmarshal([]byte(response.QrCode), &realQR)) - } + // Now let's fetch the original QR using the url + rr := httptest.NewRecorder() + req, err := http.NewRequest(http.MethodGet, qrLink, nil) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + require.Equal(t, http.StatusOK, rr.Code) + + require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &realQR)) assert.NotNil(t, realQR.Body) assert.Equal(t, "authentication", realQR.Body.Reason) diff --git a/internal/core/ports/connections_service.go b/internal/core/ports/connections_service.go index 0a22dffbf..4f5644134 100644 --- a/internal/core/ports/connections_service.go +++ b/internal/core/ports/connections_service.go @@ -15,28 +15,24 @@ import ( type NewGetAllConnectionsRequest struct { WithCredentials bool Query string - Pagination *pagination.Filter + Pagination pagination.Filter OrderBy sqltools.OrderByFilters } // NewGetAllRequest returns the request object for obtaining all connections func NewGetAllRequest(withCredentials *bool, query *string, page *uint, maxResults *uint, orderBy sqltools.OrderByFilters) *NewGetAllConnectionsRequest { - var ( - connQuery string - pagFilter *pagination.Filter - ) + var connQuery string + if query != nil { connQuery = *query } - if page != nil { - pagFilter = pagination.NewFilter(maxResults, page) - } + pagFilter := pagination.NewFilter(maxResults, page) return &NewGetAllConnectionsRequest{ WithCredentials: withCredentials != nil && *withCredentials, Query: connQuery, - Pagination: pagFilter, + Pagination: *pagFilter, OrderBy: orderBy, } } diff --git a/internal/repositories/connections.go b/internal/repositories/connections.go index 95476da0e..670e11363 100644 --- a/internal/repositories/connections.go +++ b/internal/repositories/connections.go @@ -173,12 +173,11 @@ func (c *connections) GetAllByIssuerID(ctx context.Context, conn db.Querier, iss _ = filter.OrderBy.Add(ports.ConnectionsCreatedAt, true) countQuery := strings.Replace(all, "##QUERYFIELDS##", "COUNT(*)", 1) all += " ORDER BY " + filter.OrderBy.String() - if filter.Pagination != nil { - if err := conn.QueryRow(ctx, countQuery, issuerDID.String()).Scan(&count); err != nil { - return nil, 0, err - } - all += fmt.Sprintf(" OFFSET %d LIMIT %d;", filter.Pagination.GetOffset(), filter.Pagination.GetLimit()) + + if err := conn.QueryRow(ctx, countQuery, issuerDID.String()).Scan(&count); err != nil { + return nil, 0, err } + all += fmt.Sprintf(" OFFSET %d LIMIT %d;", filter.Pagination.GetOffset(), filter.Pagination.GetLimit()) all = strings.Replace(all, "##QUERYFIELDS##", strings.Join(fields, ","), 1) rows, err := conn.Query(ctx, all, issuerDID.String()) @@ -200,21 +199,15 @@ func (c *connections) GetAllByIssuerID(ctx context.Context, conn db.Querier, iss domainConns = append(domainConns, domainConn) } - if filter.Pagination == nil { - count = uint(len(domainConns)) - } - return domainConns, count, nil } func (c *connections) GetAllWithCredentialsByIssuerID(ctx context.Context, conn db.Querier, issuerDID w3c.DID, filter *ports.NewGetAllConnectionsRequest) ([]*domain.Connection, uint, error) { + var count uint sqlQuery, countQuery, filters := buildGetAllWithCredentialsQueryAndFilters(issuerDID, filter) - var count uint - if filter.Pagination != nil { - if err := conn.QueryRow(ctx, countQuery, filters...).Scan(&count); err != nil { - return nil, 0, err - } + if err := conn.QueryRow(ctx, countQuery, filters...).Scan(&count); err != nil { + return nil, 0, err } rows, err := conn.Query(ctx, sqlQuery, filters...) @@ -225,9 +218,6 @@ func (c *connections) GetAllWithCredentialsByIssuerID(ctx context.Context, conn defer rows.Close() conns, err := toConnectionsWithCredentials(rows) - if filter.Pagination == nil { - count = uint(len(conns)) - } return conns, count, err } @@ -294,9 +284,7 @@ func buildGetAllWithCredentialsQueryAndFilters(issuerDID w3c.DID, filter *ports. _ = filter.OrderBy.Add(ports.ConnectionsCreatedAt, true) sqlQuery += " ORDER BY " + filter.OrderBy.String() - if filter.Pagination != nil { - sqlQuery += fmt.Sprintf(" OFFSET %d LIMIT %d;", filter.Pagination.GetOffset(), filter.Pagination.GetLimit()) - } + sqlQuery += fmt.Sprintf(" OFFSET %d LIMIT %d;", filter.Pagination.GetOffset(), filter.Pagination.GetLimit()) return sqlQuery, countQuery, sqlArgs } diff --git a/internal/repositories/tests/connections_test.go b/internal/repositories/tests/connections_test.go index 12f7f1172..25745d34b 100644 --- a/internal/repositories/tests/connections_test.go +++ b/internal/repositories/tests/connections_test.go @@ -170,43 +170,43 @@ func TestConnectionsGetAllByIssuerID(t *testing.T) { t.Run("should get 1 connection for a the given issuerDID and no query", func(t *testing.T) { conns, _, err := connectionsRepo.GetAllByIssuerID(ctx, storage.Pgx, *issuerDID, &ports.NewGetAllConnectionsRequest{Query: ""}) require.NoError(t, err) - assert.Equal(t, len(conns), 1) + assert.Equal(t, 1, len(conns)) }) t.Run("should get 1 connection for a the given issuerDID and valid query, just beginning", func(t *testing.T) { conns, _, err := connectionsRepo.GetAllByIssuerID(ctx, storage.Pgx, *issuerDID, &ports.NewGetAllConnectionsRequest{Query: "did:"}) require.NoError(t, err) - assert.Equal(t, len(conns), 1) + assert.Equal(t, 1, len(conns)) }) t.Run("should get 1 connection for a the given issuerDID and valid query, full did", func(t *testing.T) { conns, _, err := connectionsRepo.GetAllByIssuerID(ctx, storage.Pgx, *issuerDID, &ports.NewGetAllConnectionsRequest{Query: "did:polygonid:polygon:mumbai:2qH7XAwYQzCp9VfhpNgeLtK2iCehDDrfMWUCEg5ig5"}) require.NoError(t, err) - assert.Equal(t, len(conns), 1) + assert.Equal(t, 1, len(conns)) }) t.Run("should get 1 connection for a the given issuerDID and valid query, part of did", func(t *testing.T) { conns, _, err := connectionsRepo.GetAllByIssuerID(ctx, storage.Pgx, *issuerDID, &ports.NewGetAllConnectionsRequest{Query: "did:polygonid:polygon:mumbai:2qH7XAw"}) require.NoError(t, err) - assert.Equal(t, len(conns), 1) + assert.Equal(t, 1, len(conns)) }) t.Run("should get 1 connection for a the given issuerDID and a query with some chars in the middle of a string", func(t *testing.T) { conns, _, err := connectionsRepo.GetAllByIssuerID(ctx, storage.Pgx, *issuerDID, &ports.NewGetAllConnectionsRequest{Query: "H7XAw"}) require.NoError(t, err) - assert.Equal(t, len(conns), 1) + assert.Equal(t, 1, len(conns)) }) t.Run("should get 1 connection for a the given issuerDID and a query with some chars in the middle of a string and other words", func(t *testing.T) { conns, _, err := connectionsRepo.GetAllByIssuerID(ctx, storage.Pgx, *issuerDID, &ports.NewGetAllConnectionsRequest{Query: "H7XAw other words"}) require.NoError(t, err) - assert.Equal(t, len(conns), 1) + assert.Equal(t, 1, len(conns)) }) t.Run("should get 0 connections for a the given issuerDID and non existing userDID", func(t *testing.T) { conns, _, err := connectionsRepo.GetAllByIssuerID(ctx, storage.Pgx, *issuerDID, &ports.NewGetAllConnectionsRequest{Query: "did:polygonid:polygon:mumbai:2qH7XAwnonexisting"}) require.NoError(t, err) - assert.Equal(t, len(conns), 0) + assert.Equal(t, 0, len(conns)) }) } diff --git a/pkg/pagination/pagination.go b/pkg/pagination/pagination.go index 1e7557265..ad34add2c 100644 --- a/pkg/pagination/pagination.go +++ b/pkg/pagination/pagination.go @@ -1,6 +1,6 @@ package pagination -const defaultMaxResults = 50 +const defaultMaxResults = 1000 // Filter is a struct that contains the pagination filter type Filter struct { @@ -10,23 +10,28 @@ type Filter struct { // NewFilter creates a new filter func NewFilter(maxResults *uint, page *uint) *Filter { - var maxR uint = defaultMaxResults - if maxResults != nil { - maxR = *maxResults - } - - return &Filter{ - MaxResults: maxR, + f := &Filter{ + MaxResults: defaultMaxResults, Page: page, } + if maxResults != nil { + f.MaxResults = *maxResults + } + return f } // GetLimit returns the limit for the query func (f *Filter) GetLimit() uint { + if f.MaxResults == 0 { + return defaultMaxResults + } return f.MaxResults } // GetOffset returns the offset for the query func (f *Filter) GetOffset() uint { + if f.Page == nil { + return 0 + } return (*f.Page - 1) * f.MaxResults } diff --git a/ui/src/adapters/api/connections.ts b/ui/src/adapters/api/connections.ts index 8b0db358f..e0c94b1e3 100644 --- a/ui/src/adapters/api/connections.ts +++ b/ui/src/adapters/api/connections.ts @@ -4,10 +4,15 @@ import { z } from "zod"; import { Response, buildErrorResponse, buildSuccessResponse } from "src/adapters"; import { Message, buildAuthorizationHeader, messageParser } from "src/adapters/api"; import { credentialParser } from "src/adapters/api/credentials"; -import { datetimeParser, getListParser, getStrictParser } from "src/adapters/parsers"; +import { + datetimeParser, + getListParser, + getResourceParser, + getStrictParser, +} from "src/adapters/parsers"; import { Connection, Env } from "src/domain"; import { API_VERSION, QUERY_SEARCH_PARAM } from "src/utils/constants"; -import { List } from "src/utils/types"; +import { Resource } from "src/utils/types"; type ConnectionInput = Omit & { createdAt: string; @@ -52,16 +57,18 @@ export async function getConnection({ export async function getConnections({ credentials, env, - params, + params: { maxResults, page, query }, signal, }: { credentials: boolean; env: Env; - params?: { + params: { + maxResults?: number; + page?: number; query?: string; }; signal?: AbortSignal; -}): Promise>> { +}): Promise>> { try { const response = await axios({ baseURL: env.api.url, @@ -70,20 +77,15 @@ export async function getConnections({ }, method: "GET", params: new URLSearchParams({ - ...(params?.query !== undefined ? { [QUERY_SEARCH_PARAM]: params?.query } : {}), + ...(query !== undefined ? { [QUERY_SEARCH_PARAM]: query } : {}), ...(credentials ? { credentials: "true" } : {}), + ...(maxResults !== undefined ? { max_results: maxResults.toString() } : {}), + ...(page !== undefined ? { page: page.toString() } : {}), }), signal, url: `${API_VERSION}/connections`, }); - return buildSuccessResponse( - getListParser(connectionParser) - .transform(({ failed, successful }) => ({ - failed, - successful: successful.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()), - })) - .parse(response.data) - ); + return buildSuccessResponse(getResourceParser(connectionParser).parse(response.data)); } catch (error) { return buildErrorResponse(error); } diff --git a/ui/src/adapters/api/credentials.ts b/ui/src/adapters/api/credentials.ts index d5ee10063..d114bdf83 100644 --- a/ui/src/adapters/api/credentials.ts +++ b/ui/src/adapters/api/credentials.ts @@ -121,17 +121,7 @@ export async function getCredentials({ signal, url: `${API_VERSION}/credentials`, }); - return buildSuccessResponse( - getResourceParser(credentialParser) - .transform(({ items: { failed, successful }, meta }) => ({ - items: { - failed, - successful, - }, - meta, - })) - .parse(response.data) - ); + return buildSuccessResponse(getResourceParser(credentialParser).parse(response.data)); } catch (error) { return buildErrorResponse(error); } @@ -396,14 +386,16 @@ type AuthQRCodeInput = Omit & { export type AuthQRCode = { linkDetail: { proofTypes: ProofType[]; schemaType: string }; - qrCode: string; + qrCodeLink: string; + qrCodeRaw: string; sessionID: string; }; const authQRCodeParser = getStrictParser()( z.object({ linkDetail: z.object({ proofTypes: proofTypeParser, schemaType: z.string() }), - qrCode: z.string(), + qrCodeLink: z.string(), + qrCodeRaw: z.string(), sessionID: z.string(), }) ); @@ -430,14 +422,21 @@ export async function createAuthQRCode({ } } -const issuedQRCodeParser = getStrictParser()( - z.object({ - qrCodeLink: z.string(), - schemaType: z.string(), - }) +type IssuedQRCodeInput = { + qrCodeLink: string; + schemaType: string; +}; + +const issuedQRCodeParser = getStrictParser()( + z + .object({ + qrCodeLink: z.string(), + schemaType: z.string(), + }) + .transform(({ qrCodeLink, schemaType }) => ({ qrCode: qrCodeLink, schemaType: schemaType })) ); -export async function getIssuedQRCode({ +export async function getIssuedQRCodes({ credentialID, env, signal, @@ -445,15 +444,29 @@ export async function getIssuedQRCode({ credentialID: string; env: Env; signal: AbortSignal; -}): Promise> { +}): Promise> { try { - const response = await axios({ - baseURL: env.api.url, - method: "GET", - signal, - url: `${API_VERSION}/credentials/${credentialID}/qrcode`, - }); - return buildSuccessResponse(issuedQRCodeParser.parse(response.data)); + const [qrLinkResponse, qrRawResponse] = await Promise.all([ + axios({ + baseURL: env.api.url, + method: "GET", + params: { type: "link" }, + signal, + url: `${API_VERSION}/credentials/${credentialID}/qrcode`, + }), + axios({ + baseURL: env.api.url, + method: "GET", + params: { type: "raw" }, + signal, + url: `${API_VERSION}/credentials/${credentialID}/qrcode`, + }), + ]); + + return buildSuccessResponse([ + issuedQRCodeParser.parse(qrLinkResponse.data), + issuedQRCodeParser.parse(qrRawResponse.data), + ]); } catch (error) { return buildErrorResponse(error); } diff --git a/ui/src/components/connections/ConnectionsTable.tsx b/ui/src/components/connections/ConnectionsTable.tsx index 92fb6d0a9..2507a36c9 100644 --- a/ui/src/components/connections/ConnectionsTable.tsx +++ b/ui/src/components/connections/ConnectionsTable.tsx @@ -15,6 +15,7 @@ import { useCallback, useEffect, useState } from "react"; import { generatePath, useNavigate, useSearchParams } from "react-router-dom"; import { getConnections } from "src/adapters/api/connections"; +import { positiveIntegerFromStringParser } from "src/adapters/parsers"; import IconCreditCardPlus from "src/assets/icons/credit-card-plus.svg?react"; import IconDots from "src/assets/icons/dots-vertical.svg?react"; import IconInfoCircle from "src/assets/icons/info-circle.svg?react"; @@ -32,11 +33,16 @@ import { AsyncTask, isAsyncTaskDataAvailable, isAsyncTaskStarting } from "src/ut import { isAbortedError, makeRequestAbortable } from "src/utils/browser"; import { CONNECTIONS, + DEFAULT_PAGINATION_MAX_RESULTS, + DEFAULT_PAGINATION_PAGE, + DEFAULT_PAGINATION_TOTAL, DELETE, DETAILS, DID_SEARCH_PARAM, IDENTIFIER, ISSUED_CREDENTIALS, + PAGINATION_MAX_RESULTS_PARAM, + PAGINATION_PAGE_PARAM, QUERY_SEARCH_PARAM, } from "src/utils/constants"; import { notifyParseErrors } from "src/utils/error"; @@ -53,6 +59,22 @@ export function ConnectionsTable() { const [searchParams, setSearchParams] = useSearchParams(); + const paginationPageParsed = positiveIntegerFromStringParser.safeParse( + searchParams.get(PAGINATION_PAGE_PARAM) + ); + const paginationMaxResultsParsed = positiveIntegerFromStringParser.safeParse( + searchParams.get(PAGINATION_MAX_RESULTS_PARAM) + ); + + const [paginationTotal, setPaginationTotal] = useState(DEFAULT_PAGINATION_TOTAL); + + const paginationPage = paginationPageParsed.success + ? paginationPageParsed.data + : DEFAULT_PAGINATION_PAGE; + const paginationMaxResults = paginationMaxResultsParsed.success + ? paginationMaxResultsParsed.data + : DEFAULT_PAGINATION_MAX_RESULTS; + const queryParam = searchParams.get(QUERY_SEARCH_PARAM); const tableColumns: TableColumnsType = [ @@ -134,6 +156,28 @@ export function ConnectionsTable() { }, ]; + const updatePaginationParams = useCallback( + (pagination: { maxResults?: number; page?: number }) => { + setSearchParams((previousParams) => { + const params = new URLSearchParams(previousParams); + params.set( + PAGINATION_PAGE_PARAM, + pagination.page !== undefined + ? pagination.page.toString() + : DEFAULT_PAGINATION_PAGE.toString() + ); + params.set( + PAGINATION_MAX_RESULTS_PARAM, + pagination.maxResults !== undefined + ? pagination.maxResults.toString() + : DEFAULT_PAGINATION_MAX_RESULTS.toString() + ); + return params; + }); + }, + [setSearchParams] + ); + const fetchConnections = useCallback( async (signal?: AbortSignal) => { setConnections({ status: "loading" }); @@ -141,20 +185,30 @@ export function ConnectionsTable() { credentials: true, env, params: { + maxResults: paginationMaxResults, + page: paginationPage, query: queryParam || undefined, }, signal, }); if (response.success) { - setConnections({ data: response.data.successful, status: "successful" }); - notifyParseErrors(response.data.failed); + setConnections({ + data: response.data.items.successful, + status: "successful", + }); + setPaginationTotal(response.data.meta.total); + updatePaginationParams({ + maxResults: response.data.meta.max_results, + page: response.data.meta.page, + }); + notifyParseErrors(response.data.items.failed); } else { if (!isAbortedError(response.error)) { setConnections({ error: response.error, status: "failed" }); } } }, - [env, queryParam] + [env, paginationMaxResults, paginationPage, queryParam, updatePaginationParams] ); const onSearch = useCallback( @@ -233,7 +287,17 @@ export function ConnectionsTable() { ), }} - pagination={false} + onChange={({ current, pageSize, total }) => { + setPaginationTotal(total || DEFAULT_PAGINATION_TOTAL); + updatePaginationParams({ maxResults: pageSize, page: current }); + }} + pagination={{ + current: paginationPage, + hideOnSinglePage: true, + pageSize: paginationMaxResults, + position: ["bottomRight"], + total: paginationTotal, + }} rowKey="id" showSorterTooltip sortDirections={["ascend", "descend"]} diff --git a/ui/src/components/credentials/CredentialIssuedQR.tsx b/ui/src/components/credentials/CredentialIssuedQR.tsx index e929a8a98..a6e4e142e 100644 --- a/ui/src/components/credentials/CredentialIssuedQR.tsx +++ b/ui/src/components/credentials/CredentialIssuedQR.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { useParams } from "react-router-dom"; -import { getIssuedQRCode } from "src/adapters/api/credentials"; +import { getIssuedQRCodes } from "src/adapters/api/credentials"; import { CredentialQR } from "src/components/credentials/CredentialQR"; import { ErrorResult } from "src/components/shared/ErrorResult"; import { LoadingResult } from "src/components/shared/LoadingResult"; @@ -13,7 +13,9 @@ import { isAbortedError, makeRequestAbortable } from "src/utils/browser"; export function CredentialIssuedQR() { const env = useEnvContext(); - const [issuedQRCode, setIssuedQRCode] = useState>({ + const [issuedQRCodes, setIssuedQRCodes] = useState< + AsyncTask<[IssuedQRCode, IssuedQRCode], AppError> + >({ status: "pending", }); @@ -22,15 +24,15 @@ export function CredentialIssuedQR() { const createCredentialQR = useCallback( async (signal: AbortSignal) => { if (credentialID) { - setIssuedQRCode({ status: "loading" }); + setIssuedQRCodes({ status: "loading" }); - const response = await getIssuedQRCode({ credentialID, env, signal }); + const response = await getIssuedQRCodes({ credentialID, env, signal }); if (response.success) { - setIssuedQRCode({ data: response.data, status: "successful" }); + setIssuedQRCodes({ data: response.data, status: "successful" }); } else { if (!isAbortedError(response.error)) { - setIssuedQRCode({ error: response.error, status: "failed" }); + setIssuedQRCodes({ error: response.error, status: "failed" }); } } } @@ -46,27 +48,29 @@ export function CredentialIssuedQR() { const onStartAgain = () => { makeRequestAbortable(createCredentialQR); - setIssuedQRCode({ status: "pending" }); + setIssuedQRCodes({ status: "pending" }); }; - if (hasAsyncTaskFailed(issuedQRCode)) { + if (hasAsyncTaskFailed(issuedQRCodes)) { return ( ); } - if (!isAsyncTaskDataAvailable(issuedQRCode)) { + if (!isAsyncTaskDataAvailable(issuedQRCodes)) { return ; } + const [issuedQRCodeLink, issuedQRCodeRaw] = issuedQRCodes.data; return ( ); diff --git a/ui/src/components/credentials/CredentialLinkQR.tsx b/ui/src/components/credentials/CredentialLinkQR.tsx index 0738fcc0d..761654017 100644 --- a/ui/src/components/credentials/CredentialLinkQR.tsx +++ b/ui/src/components/credentials/CredentialLinkQR.tsx @@ -239,7 +239,8 @@ export function CredentialLinkQR() { return ( diff --git a/ui/src/components/credentials/CredentialQR.tsx b/ui/src/components/credentials/CredentialQR.tsx index 003740740..7c401d14b 100644 --- a/ui/src/components/credentials/CredentialQR.tsx +++ b/ui/src/components/credentials/CredentialQR.tsx @@ -1,4 +1,4 @@ -import { Avatar, Card, Col, Grid, Image, Row, Space, Typography } from "antd"; +import { Avatar, Card, Col, Grid, Image, Row, Space, Tabs, TabsProps, Typography } from "antd"; import { QRCodeSVG } from "qrcode.react"; import { ReactNode } from "react"; @@ -6,11 +6,13 @@ import { useEnvContext } from "src/contexts/Env"; import { WALLET_APP_STORE_URL, WALLET_PLAY_STORE_URL } from "src/utils/constants"; export function CredentialQR({ - qrCode, + qrCodeLink, + qrCodeRaw, schemaType, subTitle, }: { - qrCode: string; + qrCodeLink: string; + qrCodeRaw: string; schemaType: string; subTitle: ReactNode; }) { @@ -18,6 +20,50 @@ export function CredentialQR({ const { lg } = Grid.useBreakpoint(); + const qrCodeBase64 = `iden3comm://?i_m=${btoa(qrCodeRaw)}`; + + const qrCodeTabs: TabsProps["items"] = [ + { + children: ( + + ), + key: "1", + label: "Link", + }, + { + children: ( + + ), + key: "2", + label: "Raw JSON", + }, + { + children: ( + + ), + key: "3", + label: "Base64 encoded", + }, + ]; + return ( @@ -42,28 +88,13 @@ export function CredentialQR({ - + - - - - - - {schemaType && ( { - const response = await getConnections({ credentials: false, env, signal }); + const response = await getConnections({ + credentials: false, + env, + params: {}, + signal, + }); if (response.success) { - setConnections({ data: response.data.successful, status: "successful" }); - notifyParseErrors(response.data.failed); + setConnections({ data: response.data.items.successful, status: "successful" }); + notifyParseErrors(response.data.items.failed); } else { setConnections({ error: response.error, status: "failed" }); } diff --git a/ui/src/domain/credential.ts b/ui/src/domain/credential.ts index 090b58a88..77526cdfa 100644 --- a/ui/src/domain/credential.ts +++ b/ui/src/domain/credential.ts @@ -18,7 +18,7 @@ export type Credential = { }; export type IssuedQRCode = { - qrCodeLink: string; + qrCode: string; schemaType: string; };