feat(client): add KYC for event joining

Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
2026-02-05 19:12:57 +08:00
committed by Asai Neko
parent f793a7516f
commit 69a7756886
31 changed files with 1760 additions and 187 deletions

View File

@@ -3,8 +3,8 @@
import { type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
import { client } from '../client.gen';
import { getAuthRedirect, getEventCheckin, getEventCheckinQuery, getEventInfo, getEventList, getUserInfo, getUserInfoByUserId, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit, postEventJoin, postKycQuery, postKycSession } from '../sdk.gen';
import type { GetAuthRedirectData, GetAuthRedirectError, GetEventCheckinData, GetEventCheckinError, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryResponse, GetEventCheckinResponse, GetEventInfoData, GetEventInfoError, GetEventInfoResponse, GetEventListData, GetEventListError, GetEventListResponse, GetUserInfoByUserIdData, GetUserInfoByUserIdError, GetUserInfoByUserIdResponse, GetUserInfoData, GetUserInfoError, GetUserInfoResponse, GetUserListData, GetUserListError, GetUserListResponse, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateResponse, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeResponse, PostAuthMagicData, PostAuthMagicError, PostAuthMagicResponse, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshResponse, PostAuthTokenData, PostAuthTokenError, PostAuthTokenResponse, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitResponse, PostEventJoinData, PostEventJoinError, PostEventJoinResponse, PostKycQueryData, PostKycQueryError, PostKycQueryResponse, PostKycSessionData, PostKycSessionError, PostKycSessionResponse } from '../types.gen';
import { getAuthRedirect, getEventAttendance, getEventCheckin, getEventCheckinQuery, getEventInfo, getEventList, getUserInfo, getUserInfoByUserId, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit, postEventJoin, postKycQuery, postKycSession } from '../sdk.gen';
import type { GetAuthRedirectData, GetAuthRedirectError, GetEventAttendanceData, GetEventAttendanceError, GetEventAttendanceResponse, GetEventCheckinData, GetEventCheckinError, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryResponse, GetEventCheckinResponse, GetEventInfoData, GetEventInfoError, GetEventInfoResponse, GetEventListData, GetEventListError, GetEventListResponse, GetUserInfoByUserIdData, GetUserInfoByUserIdError, GetUserInfoByUserIdResponse, GetUserInfoData, GetUserInfoError, GetUserInfoResponse, GetUserListData, GetUserListError, GetUserListResponse, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateResponse, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeResponse, PostAuthMagicData, PostAuthMagicError, PostAuthMagicResponse, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshResponse, PostAuthTokenData, PostAuthTokenError, PostAuthTokenResponse, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitResponse, PostEventJoinData, PostEventJoinError, PostEventJoinResponse, PostKycQueryData, PostKycQueryError, PostKycQueryResponse, PostKycSessionData, PostKycSessionError, PostKycSessionResponse } from '../types.gen';
/**
* Exchange Auth Code
@@ -135,6 +135,26 @@ export const postAuthTokenMutation = (options?: Partial<Options<PostAuthTokenDat
return mutationOptions;
};
export const getEventAttendanceQueryKey = (options: Options<GetEventAttendanceData>) => createQueryKey('getEventAttendance', options);
/**
* Get Attendance List
*
* Retrieves the list of attendees, including user info and decrypted KYC data for a specified event.
*/
export const getEventAttendanceOptions = (options: Options<GetEventAttendanceData>) => queryOptions<GetEventAttendanceResponse, GetEventAttendanceError, GetEventAttendanceResponse, ReturnType<typeof getEventAttendanceQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getEventAttendance({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getEventAttendanceQueryKey(options)
});
export const getEventCheckinQueryKey = (options: Options<GetEventCheckinData>) => createQueryKey('getEventCheckin', options);
/**
@@ -289,7 +309,7 @@ export const getEventListInfiniteQueryKey = (options: Options<GetEventListData>)
*
* Fetches a list of events with support for pagination via limit and offset. Data is retrieved directly from the database for consistency.
*/
export const getEventListInfiniteOptions = (options: Options<GetEventListData>) => infiniteQueryOptions<GetEventListResponse, GetEventListError, InfiniteData<GetEventListResponse>, QueryKey<Options<GetEventListData>>, string | Pick<QueryKey<Options<GetEventListData>>[0], 'body' | 'headers' | 'path' | 'query'>>(
export const getEventListInfiniteOptions = (options: Options<GetEventListData>) => infiniteQueryOptions<GetEventListResponse, GetEventListError, InfiniteData<GetEventListResponse>, QueryKey<Options<GetEventListData>>, number | Pick<QueryKey<Options<GetEventListData>>[0], 'body' | 'headers' | 'path' | 'query'>>(
// @ts-ignore
{
queryFn: async ({ pageParam, queryKey, signal }) => {
@@ -349,14 +369,14 @@ export const postKycSessionMutation = (options?: Partial<Options<PostKycSessionD
return mutationOptions;
};
export const getUserInfoQueryKey = (options?: Options<GetUserInfoData>) => createQueryKey('getUserInfo', options);
export const getUserInfoQueryKey = (options: Options<GetUserInfoData>) => createQueryKey('getUserInfo', options);
/**
* Get My User Information
*
* Fetches the complete profile data for the user associated with the provided session/token.
*/
export const getUserInfoOptions = (options?: Options<GetUserInfoData>) => queryOptions<GetUserInfoResponse, GetUserInfoError, GetUserInfoResponse, ReturnType<typeof getUserInfoQueryKey>>({
export const getUserInfoOptions = (options: Options<GetUserInfoData>) => queryOptions<GetUserInfoResponse, GetUserInfoError, GetUserInfoResponse, ReturnType<typeof getUserInfoQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getUserInfo({
...options,

View File

@@ -1,4 +1,4 @@
// This file is auto-generated by @hey-api/openapi-ts
export { getAuthRedirect, getEventCheckin, getEventCheckinQuery, getEventInfo, getEventList, getUserInfo, getUserInfoByUserId, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit, postEventJoin, postKycQuery, postKycSession } from './sdk.gen';
export type { ClientOptions, DataEventIndexDoc, DataUserIndexDoc, GetAuthRedirectData, GetAuthRedirectError, GetAuthRedirectErrors, GetEventCheckinData, GetEventCheckinError, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryErrors, GetEventCheckinQueryResponse, GetEventCheckinQueryResponses, GetEventCheckinResponse, GetEventCheckinResponses, GetEventInfoData, GetEventInfoError, GetEventInfoErrors, GetEventInfoResponse, GetEventInfoResponses, GetEventListData, GetEventListError, GetEventListErrors, GetEventListResponse, GetEventListResponses, GetUserInfoByUserIdData, GetUserInfoByUserIdError, GetUserInfoByUserIdErrors, GetUserInfoByUserIdResponse, GetUserInfoByUserIdResponses, GetUserInfoData, GetUserInfoError, GetUserInfoErrors, GetUserInfoResponse, GetUserInfoResponses, GetUserListData, GetUserListError, GetUserListErrors, GetUserListResponse, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateErrors, PatchUserUpdateResponse, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeErrors, PostAuthExchangeResponse, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicError, PostAuthMagicErrors, PostAuthMagicResponse, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshErrors, PostAuthRefreshResponse, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenError, PostAuthTokenErrors, PostAuthTokenResponse, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponse, PostEventCheckinSubmitResponses, PostEventJoinData, PostEventJoinError, PostEventJoinErrors, PostEventJoinResponse, PostEventJoinResponses, PostKycQueryData, PostKycQueryError, PostKycQueryErrors, PostKycQueryResponse, PostKycQueryResponses, PostKycSessionData, PostKycSessionError, PostKycSessionErrors, PostKycSessionResponse, PostKycSessionResponses, ServiceAuthExchangeData, ServiceAuthExchangeResponse, ServiceAuthMagicData, ServiceAuthMagicResponse, ServiceAuthRefreshData, ServiceAuthTokenData, ServiceAuthTokenResponse, ServiceEventCheckinQueryResponse, ServiceEventCheckinResponse, ServiceEventCheckinSubmitData, ServiceEventEventJoinData, ServiceKycKycQueryData, ServiceKycKycQueryResponse, ServiceKycKycSessionData, ServiceKycKycSessionResponse, ServiceUserUserInfoData, UtilsRespStatus } from './types.gen';
export { getAuthRedirect, getEventAttendance, getEventCheckin, getEventCheckinQuery, getEventInfo, getEventList, getUserInfo, getUserInfoByUserId, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit, postEventJoin, postKycQuery, postKycSession } from './sdk.gen';
export type { ClientOptions, DataEventIndexDoc, DataUserIndexDoc, GetAuthRedirectData, GetAuthRedirectError, GetAuthRedirectErrors, GetEventAttendanceData, GetEventAttendanceError, GetEventAttendanceErrors, GetEventAttendanceResponse, GetEventAttendanceResponses, GetEventCheckinData, GetEventCheckinError, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryErrors, GetEventCheckinQueryResponse, GetEventCheckinQueryResponses, GetEventCheckinResponse, GetEventCheckinResponses, GetEventInfoData, GetEventInfoError, GetEventInfoErrors, GetEventInfoResponse, GetEventInfoResponses, GetEventListData, GetEventListError, GetEventListErrors, GetEventListResponse, GetEventListResponses, GetUserInfoByUserIdData, GetUserInfoByUserIdError, GetUserInfoByUserIdErrors, GetUserInfoByUserIdResponse, GetUserInfoByUserIdResponses, GetUserInfoData, GetUserInfoError, GetUserInfoErrors, GetUserInfoResponse, GetUserInfoResponses, GetUserListData, GetUserListError, GetUserListErrors, GetUserListResponse, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateErrors, PatchUserUpdateResponse, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeErrors, PostAuthExchangeResponse, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicError, PostAuthMagicErrors, PostAuthMagicResponse, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshErrors, PostAuthRefreshResponse, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenError, PostAuthTokenErrors, PostAuthTokenResponse, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponse, PostEventCheckinSubmitResponses, PostEventJoinData, PostEventJoinError, PostEventJoinErrors, PostEventJoinResponse, PostEventJoinResponses, PostKycQueryData, PostKycQueryError, PostKycQueryErrors, PostKycQueryResponse, PostKycQueryResponses, PostKycSessionData, PostKycSessionError, PostKycSessionErrors, PostKycSessionResponse, PostKycSessionResponses, ServiceAuthExchangeData, ServiceAuthExchangeResponse, ServiceAuthMagicData, ServiceAuthMagicResponse, ServiceAuthRefreshData, ServiceAuthTokenData, ServiceAuthTokenResponse, ServiceEventAttendanceListResponse, ServiceEventCheckinQueryResponse, ServiceEventCheckinResponse, ServiceEventCheckinSubmitData, ServiceEventEventJoinData, ServiceKycKycQueryData, ServiceKycKycQueryResponse, ServiceKycKycSessionData, ServiceKycKycSessionResponse, ServiceUserUserInfoData, UtilsRespStatus } from './types.gen';

View File

@@ -2,7 +2,7 @@
import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen';
import type { GetAuthRedirectData, GetAuthRedirectErrors, GetEventCheckinData, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryErrors, GetEventCheckinQueryResponses, GetEventCheckinResponses, GetEventInfoData, GetEventInfoErrors, GetEventInfoResponses, GetEventListData, GetEventListErrors, GetEventListResponses, GetUserInfoByUserIdData, GetUserInfoByUserIdErrors, GetUserInfoByUserIdResponses, GetUserInfoData, GetUserInfoErrors, GetUserInfoResponses, GetUserListData, GetUserListErrors, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateErrors, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeErrors, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicErrors, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshErrors, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenErrors, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponses, PostEventJoinData, PostEventJoinErrors, PostEventJoinResponses, PostKycQueryData, PostKycQueryErrors, PostKycQueryResponses, PostKycSessionData, PostKycSessionErrors, PostKycSessionResponses } from './types.gen';
import type { GetAuthRedirectData, GetAuthRedirectErrors, GetEventAttendanceData, GetEventAttendanceErrors, GetEventAttendanceResponses, GetEventCheckinData, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryErrors, GetEventCheckinQueryResponses, GetEventCheckinResponses, GetEventInfoData, GetEventInfoErrors, GetEventInfoResponses, GetEventListData, GetEventListErrors, GetEventListResponses, GetUserInfoByUserIdData, GetUserInfoByUserIdErrors, GetUserInfoByUserIdResponses, GetUserInfoData, GetUserInfoErrors, GetUserInfoResponses, GetUserListData, GetUserListErrors, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateErrors, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeErrors, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicErrors, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshErrors, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenErrors, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponses, PostEventJoinData, PostEventJoinErrors, PostEventJoinResponses, PostKycQueryData, PostKycQueryErrors, PostKycQueryResponses, PostKycSessionData, PostKycSessionErrors, PostKycSessionResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/**
@@ -81,6 +81,13 @@ export const postAuthToken = <ThrowOnError extends boolean = false>(options: Opt
}
});
/**
* Get Attendance List
*
* Retrieves the list of attendees, including user info and decrypted KYC data for a specified event.
*/
export const getEventAttendance = <ThrowOnError extends boolean = false>(options: Options<GetEventAttendanceData, ThrowOnError>) => (options.client ?? client).get<GetEventAttendanceResponses, GetEventAttendanceErrors, ThrowOnError>({ url: '/event/attendance', ...options });
/**
* Generate Check-in Code
*
@@ -170,7 +177,7 @@ export const postKycSession = <ThrowOnError extends boolean = false>(options: Op
*
* Fetches the complete profile data for the user associated with the provided session/token.
*/
export const getUserInfo = <ThrowOnError extends boolean = false>(options?: Options<GetUserInfoData, ThrowOnError>) => (options?.client ?? client).get<GetUserInfoResponses, GetUserInfoErrors, ThrowOnError>({ url: '/user/info', ...options });
export const getUserInfo = <ThrowOnError extends boolean = false>(options: Options<GetUserInfoData, ThrowOnError>) => (options.client ?? client).get<GetUserInfoResponses, GetUserInfoErrors, ThrowOnError>({ url: '/user/info', ...options });
/**
* Get Other User Information

View File

@@ -5,9 +5,13 @@ export type ClientOptions = {
};
export type DataEventIndexDoc = {
checkin_count?: number;
description?: string;
enable_kyc?: boolean;
end_time?: string;
event_id?: string;
is_joined?: boolean;
join_count?: number;
name?: string;
start_time?: string;
thumbnail?: string;
@@ -60,6 +64,13 @@ export type ServiceAuthTokenResponse = {
refresh_token?: string;
};
export type ServiceEventAttendanceListResponse = {
attendance_id?: string;
kyc_info?: unknown;
kyc_type?: string;
user_info?: ServiceUserUserInfoData;
};
export type ServiceEventCheckinQueryResponse = {
checkin_at?: string;
};
@@ -132,6 +143,12 @@ export type PostAuthExchangeData = {
* Exchange Request Credentials
*/
body: ServiceAuthExchangeData;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query?: never;
url: '/auth/exchange';
@@ -182,6 +199,12 @@ export type PostAuthMagicData = {
* Magic Link Request Data
*/
body: ServiceAuthMagicData;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query?: never;
url: '/auth/magic';
@@ -285,6 +308,12 @@ export type PostAuthRefreshData = {
* Refresh Token Body
*/
body: ServiceAuthRefreshData;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query?: never;
url: '/auth/refresh';
@@ -335,6 +364,12 @@ export type PostAuthTokenData = {
* Token Request Body
*/
body: ServiceAuthTokenData;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query?: never;
url: '/auth/token';
@@ -380,8 +415,72 @@ export type PostAuthTokenResponses = {
export type PostAuthTokenResponse = PostAuthTokenResponses[keyof PostAuthTokenResponses];
export type GetEventAttendanceData = {
body?: never;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query: {
/**
* Event UUID
*/
event_id: string;
};
url: '/event/attendance';
};
export type GetEventAttendanceErrors = {
/**
* Invalid Input
*/
400: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Unauthorized
*/
401: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Internal Server Error
*/
500: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type GetEventAttendanceError = GetEventAttendanceErrors[keyof GetEventAttendanceErrors];
export type GetEventAttendanceResponses = {
/**
* Successful retrieval
*/
200: UtilsRespStatus & {
data?: Array<ServiceEventAttendanceListResponse>;
};
};
export type GetEventAttendanceResponse = GetEventAttendanceResponses[keyof GetEventAttendanceResponses];
export type GetEventCheckinData = {
body?: never;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query: {
/**
@@ -514,6 +613,12 @@ export type PostEventCheckinSubmitResponse = PostEventCheckinSubmitResponses[key
export type GetEventInfoData = {
body?: never;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query: {
/**
@@ -577,6 +682,12 @@ export type PostEventJoinData = {
* Event Join Details (UserId and EventId are required)
*/
body: ServiceEventEventJoinData;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query?: never;
url: '/event/join';
@@ -600,7 +711,7 @@ export type PostEventJoinErrors = {
};
};
/**
* Unauthorized / Missing User ID
* Unauthorized / Missing User ID / Event Limit Exceeded
*/
403: UtilsRespStatus & {
data?: {
@@ -634,16 +745,22 @@ export type PostEventJoinResponse = PostEventJoinResponses[keyof PostEventJoinRe
export type GetEventListData = {
body?: never;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query: {
query?: {
/**
* Maximum number of events to return (default 20)
*/
limit?: string;
limit?: number;
/**
* Number of events to skip
*/
offset: string;
offset?: number;
};
url: '/event/list';
};
@@ -693,6 +810,12 @@ export type PostKycQueryData = {
* KYC query data (KycId)
*/
body: ServiceKycKycQueryData;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query?: never;
url: '/kyc/query';
@@ -743,6 +866,12 @@ export type PostKycSessionData = {
* KYC session data (Type and Base64 Identity)
*/
body: ServiceKycKycSessionData;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query?: never;
url: '/kyc/session';
@@ -790,6 +919,12 @@ export type PostKycSessionResponse = PostKycSessionResponses[keyof PostKycSessio
export type GetUserInfoData = {
body?: never;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query?: never;
url: '/user/info';
@@ -837,6 +972,12 @@ export type GetUserInfoResponse = GetUserInfoResponses[keyof GetUserInfoResponse
export type GetUserInfoByUserIdData = {
body?: never;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path: {
/**
* Other user id
@@ -897,6 +1038,12 @@ export type GetUserInfoByUserIdResponse = GetUserInfoByUserIdResponses[keyof Get
export type GetUserListData = {
body?: never;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query: {
/**
@@ -956,6 +1103,12 @@ export type PatchUserUpdateData = {
* Updated User Profile Data
*/
body: ServiceUserUserInfoData;
headers: {
/**
* latest
*/
'X-Api-Version': string;
};
path?: never;
query?: never;
url: '/user/update';

View File

@@ -3,184 +3,228 @@
import { z } from 'zod';
export const zDataEventIndexDoc = z.object({
description: z.optional(z.string()),
end_time: z.optional(z.string()),
event_id: z.optional(z.string()),
name: z.optional(z.string()),
start_time: z.optional(z.string()),
thumbnail: z.optional(z.string()),
type: z.optional(z.string())
checkin_count: z.number().int().optional(),
description: z.string().optional(),
enable_kyc: z.boolean().optional(),
end_time: z.string().optional(),
event_id: z.string().optional(),
is_joined: z.boolean().optional(),
join_count: z.number().int().optional(),
name: z.string().optional(),
start_time: z.string().optional(),
thumbnail: z.string().optional(),
type: z.string().optional()
});
export const zDataUserIndexDoc = z.object({
avatar: z.optional(z.string()),
email: z.optional(z.string()),
nickname: z.optional(z.string()),
subtitle: z.optional(z.string()),
type: z.optional(z.string()),
user_id: z.optional(z.string()),
username: z.optional(z.string())
avatar: z.string().optional(),
email: z.string().optional(),
nickname: z.string().optional(),
subtitle: z.string().optional(),
type: z.string().optional(),
user_id: z.string().optional(),
username: z.string().optional()
});
export const zServiceAuthExchangeData = z.object({
client_id: z.optional(z.string()),
redirect_uri: z.optional(z.string()),
state: z.optional(z.string())
client_id: z.string().optional(),
redirect_uri: z.string().optional(),
state: z.string().optional()
});
export const zServiceAuthExchangeResponse = z.object({
redirect_uri: z.optional(z.string())
redirect_uri: z.string().optional()
});
export const zServiceAuthMagicData = z.object({
client_id: z.optional(z.string()),
client_ip: z.optional(z.string()),
email: z.optional(z.string()),
redirect_uri: z.optional(z.string()),
state: z.optional(z.string()),
turnstile_token: z.optional(z.string())
client_id: z.string().optional(),
client_ip: z.string().optional(),
email: z.string().optional(),
redirect_uri: z.string().optional(),
state: z.string().optional(),
turnstile_token: z.string().optional()
});
export const zServiceAuthMagicResponse = z.object({
uri: z.optional(z.string())
uri: z.string().optional()
});
export const zServiceAuthRefreshData = z.object({
refresh_token: z.optional(z.string())
refresh_token: z.string().optional()
});
export const zServiceAuthTokenData = z.object({
code: z.optional(z.string())
code: z.string().optional()
});
export const zServiceAuthTokenResponse = z.object({
access_token: z.optional(z.string()),
refresh_token: z.optional(z.string())
access_token: z.string().optional(),
refresh_token: z.string().optional()
});
export const zServiceEventCheckinQueryResponse = z.object({
checkin_at: z.optional(z.string())
checkin_at: z.string().optional()
});
export const zServiceEventCheckinResponse = z.object({
checkin_code: z.optional(z.string())
checkin_code: z.string().optional()
});
export const zServiceEventCheckinSubmitData = z.object({
checkin_code: z.optional(z.string())
checkin_code: z.string().optional()
});
export const zServiceEventEventJoinData = z.object({
event_id: z.optional(z.string()),
kyc_id: z.optional(z.string())
event_id: z.string().optional(),
kyc_id: z.string().optional()
});
export const zServiceKycKycQueryData = z.object({
kyc_id: z.optional(z.string())
kyc_id: z.string().optional()
});
export const zServiceKycKycQueryResponse = z.object({
status: z.optional(z.string())
status: z.string().optional()
});
export const zServiceKycKycSessionData = z.object({
identity: z.optional(z.string()),
type: z.optional(z.string())
identity: z.string().optional(),
type: z.string().optional()
});
export const zServiceKycKycSessionResponse = z.object({
kyc_id: z.optional(z.string()),
redirect_uri: z.optional(z.string()),
status: z.optional(z.string())
kyc_id: z.string().optional(),
redirect_uri: z.string().optional(),
status: z.string().optional()
});
export const zServiceUserUserInfoData = z.object({
allow_public: z.optional(z.boolean()),
avatar: z.optional(z.string()),
bio: z.optional(z.string()),
email: z.optional(z.string()),
nickname: z.optional(z.string()),
permission_level: z.optional(z.int()),
subtitle: z.optional(z.string()),
user_id: z.optional(z.string()),
username: z.optional(z.string())
allow_public: z.boolean().optional(),
avatar: z.string().optional(),
bio: z.string().optional(),
email: z.string().optional(),
nickname: z.string().optional(),
permission_level: z.number().int().optional(),
subtitle: z.string().optional(),
user_id: z.string().optional(),
username: z.string().optional()
});
export const zServiceEventAttendanceListResponse = z.object({
attendance_id: z.string().optional(),
kyc_info: z.unknown().optional(),
kyc_type: z.string().optional(),
user_info: zServiceUserUserInfoData.optional()
});
export const zUtilsRespStatus = z.object({
code: z.optional(z.int()),
data: z.optional(z.unknown()),
error_id: z.optional(z.string()),
status: z.optional(z.string())
code: z.number().int().optional(),
data: z.unknown().optional(),
error_id: z.string().optional(),
status: z.string().optional()
});
export const zPostAuthExchangeData = z.object({
body: zServiceAuthExchangeData,
path: z.optional(z.never()),
query: z.optional(z.never())
path: z.never().optional(),
query: z.never().optional(),
headers: z.object({
'X-Api-Version': z.string()
})
});
/**
* Successful exchange
*/
export const zPostAuthExchangeResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceAuthExchangeResponse)
data: zServiceAuthExchangeResponse.optional()
}));
export const zPostAuthMagicData = z.object({
body: zServiceAuthMagicData,
path: z.optional(z.never()),
query: z.optional(z.never())
path: z.never().optional(),
query: z.never().optional(),
headers: z.object({
'X-Api-Version': z.string()
})
});
/**
* Successful request
*/
export const zPostAuthMagicResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceAuthMagicResponse)
data: zServiceAuthMagicResponse.optional()
}));
export const zGetAuthRedirectData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
body: z.never().optional(),
path: z.never().optional(),
query: z.object({
client_id: z.string(),
redirect_uri: z.string(),
code: z.string(),
state: z.optional(z.string())
state: z.string().optional()
})
});
export const zPostAuthRefreshData = z.object({
body: zServiceAuthRefreshData,
path: z.optional(z.never()),
query: z.optional(z.never())
path: z.never().optional(),
query: z.never().optional(),
headers: z.object({
'X-Api-Version': z.string()
})
});
/**
* Successful rotation
*/
export const zPostAuthRefreshResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceAuthTokenResponse)
data: zServiceAuthTokenResponse.optional()
}));
export const zPostAuthTokenData = z.object({
body: zServiceAuthTokenData,
path: z.optional(z.never()),
query: z.optional(z.never())
path: z.never().optional(),
query: z.never().optional(),
headers: z.object({
'X-Api-Version': z.string()
})
});
/**
* Successful token issuance
*/
export const zPostAuthTokenResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceAuthTokenResponse)
data: zServiceAuthTokenResponse.optional()
}));
export const zGetEventAttendanceData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.object({
event_id: z.string()
}),
headers: z.object({
'X-Api-Version': z.string()
})
});
/**
* Successful retrieval
*/
export const zGetEventAttendanceResponse = zUtilsRespStatus.and(z.object({
data: z.array(zServiceEventAttendanceListResponse).optional()
}));
export const zGetEventCheckinData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
body: z.never().optional(),
path: z.never().optional(),
query: z.object({
event_id: z.string()
}),
headers: z.object({
'X-Api-Version': z.string()
})
});
@@ -188,12 +232,12 @@ export const zGetEventCheckinData = z.object({
* Successfully generated code
*/
export const zGetEventCheckinResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceEventCheckinResponse)
data: zServiceEventCheckinResponse.optional()
}));
export const zGetEventCheckinQueryData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
body: z.never().optional(),
path: z.never().optional(),
query: z.object({
event_id: z.string()
})
@@ -203,27 +247,30 @@ export const zGetEventCheckinQueryData = z.object({
* Current attendance status
*/
export const zGetEventCheckinQueryResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceEventCheckinQueryResponse)
data: zServiceEventCheckinQueryResponse.optional()
}));
export const zPostEventCheckinSubmitData = z.object({
body: zServiceEventCheckinSubmitData,
path: z.optional(z.never()),
query: z.optional(z.never())
path: z.never().optional(),
query: z.never().optional()
});
/**
* Attendance marked successfully
*/
export const zPostEventCheckinSubmitResponse = zUtilsRespStatus.and(z.object({
data: z.optional(z.record(z.string(), z.unknown()))
data: z.record(z.unknown()).optional()
}));
export const zGetEventInfoData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
body: z.never().optional(),
path: z.never().optional(),
query: z.object({
event_id: z.string()
}),
headers: z.object({
'X-Api-Version': z.string()
})
});
@@ -231,28 +278,34 @@ export const zGetEventInfoData = z.object({
* Successful retrieval
*/
export const zGetEventInfoResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zDataEventIndexDoc)
data: zDataEventIndexDoc.optional()
}));
export const zPostEventJoinData = z.object({
body: zServiceEventEventJoinData,
path: z.optional(z.never()),
query: z.optional(z.never())
path: z.never().optional(),
query: z.never().optional(),
headers: z.object({
'X-Api-Version': z.string()
})
});
/**
* Successfully joined the event
*/
export const zPostEventJoinResponse = zUtilsRespStatus.and(z.object({
data: z.optional(z.record(z.string(), z.unknown()))
data: z.record(z.unknown()).optional()
}));
export const zGetEventListData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
body: z.never().optional(),
path: z.never().optional(),
query: z.object({
limit: z.optional(z.string()),
offset: z.string()
limit: z.number().int().optional(),
offset: z.number().int().optional()
}).optional(),
headers: z.object({
'X-Api-Version': z.string()
})
});
@@ -260,69 +313,84 @@ export const zGetEventListData = z.object({
* Successful paginated list retrieval
*/
export const zGetEventListResponse = zUtilsRespStatus.and(z.object({
data: z.optional(z.array(zDataEventIndexDoc))
data: z.array(zDataEventIndexDoc).optional()
}));
export const zPostKycQueryData = z.object({
body: zServiceKycKycQueryData,
path: z.optional(z.never()),
query: z.optional(z.never())
path: z.never().optional(),
query: z.never().optional(),
headers: z.object({
'X-Api-Version': z.string()
})
});
/**
* Query processed (success/pending/failed)
*/
export const zPostKycQueryResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceKycKycQueryResponse)
data: zServiceKycKycQueryResponse.optional()
}));
export const zPostKycSessionData = z.object({
body: zServiceKycKycSessionData,
path: z.optional(z.never()),
query: z.optional(z.never())
path: z.never().optional(),
query: z.never().optional(),
headers: z.object({
'X-Api-Version': z.string()
})
});
/**
* Session created successfully
*/
export const zPostKycSessionResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceKycKycSessionResponse)
data: zServiceKycKycSessionResponse.optional()
}));
export const zGetUserInfoData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
query: z.optional(z.never())
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional(),
headers: z.object({
'X-Api-Version': z.string()
})
});
/**
* Successful profile retrieval
*/
export const zGetUserInfoResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceUserUserInfoData)
data: zServiceUserUserInfoData.optional()
}));
export const zGetUserInfoByUserIdData = z.object({
body: z.optional(z.never()),
body: z.never().optional(),
path: z.object({
user_id: z.string()
}),
query: z.optional(z.never())
query: z.never().optional(),
headers: z.object({
'X-Api-Version': z.string()
})
});
/**
* Successful profile retrieval
*/
export const zGetUserInfoByUserIdResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceUserUserInfoData)
data: zServiceUserUserInfoData.optional()
}));
export const zGetUserListData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
body: z.never().optional(),
path: z.never().optional(),
query: z.object({
limit: z.optional(z.string()),
limit: z.string().optional(),
offset: z.string()
}),
headers: z.object({
'X-Api-Version': z.string()
})
});
@@ -330,18 +398,21 @@ export const zGetUserListData = z.object({
* Successful paginated list retrieval
*/
export const zGetUserListResponse = zUtilsRespStatus.and(z.object({
data: z.optional(z.array(zDataUserIndexDoc))
data: z.array(zDataUserIndexDoc).optional()
}));
export const zPatchUserUpdateData = z.object({
body: zServiceUserUserInfoData,
path: z.optional(z.never()),
query: z.optional(z.never())
path: z.never().optional(),
query: z.never().optional(),
headers: z.object({
'X-Api-Version': z.string()
})
});
/**
* Successful profile update
*/
export const zPatchUserUpdateResponse = zUtilsRespStatus.and(z.object({
data: z.optional(z.record(z.string(), z.unknown()))
data: z.record(z.unknown()).optional()
}));

View File

@@ -2,7 +2,6 @@ import type { EventInfo } from './types';
import dayjs from 'dayjs';
import { Calendar } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardAction,
@@ -13,7 +12,8 @@ import {
} from '@/components/ui/card';
import { Skeleton } from '../ui/skeleton';
export function EventCardView({ type, coverImage, eventName, description, startTime, endTime, onJoinEvent }: EventInfo) {
export function EventCardView({ eventInfo, actionFooter }: { eventInfo: EventInfo; actionFooter: React.ReactNode }) {
const { type, coverImage, eventName, description, startTime, endTime } = eventInfo;
const startDayJs = dayjs(startTime);
const endDayJs = dayjs(endTime);
return (
@@ -41,7 +41,7 @@ export function EventCardView({ type, coverImage, eventName, description, startT
</CardDescription>
</CardHeader>
<CardFooter>
<Button className="w-full" onClick={onJoinEvent}></Button>
{actionFooter}
</CardFooter>
</Card>
);

View File

@@ -1,23 +1,40 @@
import type { EventInfo } from './types';
import PlaceholderImage from '@/assets/event-placeholder.png';
import { useGetEvents } from '@/hooks/data/useGetEvents';
import { useJoinEvent } from '@/hooks/data/useJoinEvent';
import { Button } from '../ui/button';
import { DialogTrigger } from '../ui/dialog';
import { EventGridView } from './event-grid.view';
import { KycDialogContainer } from './kyc/kyc.dialog.container';
export function EventGridContainer() {
const { data, isLoading } = useGetEvents();
const { mutate } = useJoinEvent();
const allEvents: EventInfo[] = isLoading
? []
: data.pages.flatMap(page => page.data!).map(it => ({
type: it.type! as EventInfo['type'],
coverImage: it.thumbnail! || PlaceholderImage,
eventName: it.name!,
description: it.description!,
startTime: new Date(it.start_time!),
endTime: new Date(it.end_time!),
onJoinEvent: () => mutate({ body: { event_id: it.event_id } }),
} satisfies EventInfo));
type: it.type! as EventInfo['type'],
eventId: it.event_id!,
isJoined: it.is_joined!,
requireKyc: it.enable_kyc!,
coverImage: it.thumbnail! || PlaceholderImage,
eventName: it.name!,
description: it.description!,
startTime: new Date(it.start_time!),
endTime: new Date(it.end_time!),
} satisfies EventInfo));
return <EventGridView events={allEvents} />;
return (
<EventGridView
events={allEvents}
assembleFooter={eventInfo => (eventInfo.isJoined
? <Button className="w-full" disabled></Button>
: (
<KycDialogContainer eventIdToJoin={eventInfo.eventId}>
<DialogTrigger asChild>
<Button className="w-full"></Button>
</DialogTrigger>
</KycDialogContainer>
)
)}
/>
);
}

View File

@@ -1,11 +1,11 @@
import type { EventInfo } from './types';
import { EventCardView } from './event-card.view';
export function EventGridView({ events }: { events: EventInfo[] }) {
export function EventGridView({ events, assembleFooter }: { events: EventInfo[]; assembleFooter: (event: EventInfo) => React.ReactNode }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
{events.map(event => (
<EventCardView key={event.eventName} {...event} />
<EventCardView key={event.eventId} eventInfo={event} actionFooter={assembleFooter(event)} />
))}
</div>
);

View File

@@ -0,0 +1,18 @@
import { X } from 'lucide-react';
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
export function KycFailedDialogView() {
return (
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
<p></p>
<div className="flex justify-center my-12">
<X size={100} />
</div>
</DialogDescription>
</DialogHeader>
</DialogContent>
);
}

View File

@@ -0,0 +1,177 @@
import type { KycSubmission } from './kyc.types';
import { useForm } from '@tanstack/react-form';
import { useState } from 'react';
import { toast } from 'sonner';
import z from 'zod';
import {
Field,
FieldError,
FieldLabel,
} from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Button } from '../../ui/button';
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog';
const CnridSchema = z.object({
cnrid: z.string().min(18, '身份证号应为18位').max(18, '身份证号应为18位'),
name: z.string().min(2, '姓名应至少2个字符').max(10, '姓名应不超过10个字符'),
});
function CnridForm({ onSubmit }: { onSubmit: OnSubmit }) {
const form = useForm({
defaultValues: {
cnrid: '',
name: '',
},
validators: {
onSubmit: CnridSchema,
},
onSubmit: async (values) => {
await onSubmit({
method: 'cnrid',
...values.value,
}).catch(() => {
toast('认证失败,请稍后再试');
});
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
void form.handleSubmit();
}}
className="flex flex-col gap-4"
>
<form.Field name="name">
{field => (
<Field>
<FieldLabel htmlFor="name"></FieldLabel>
<Input
id="name"
name="name"
value={field.state.value}
onBlur={field.handleBlur}
onChange={e => field.handleChange(e.target.value)}
/>
<FieldError errors={field.state.meta.errors} />
</Field>
)}
</form.Field>
<form.Field name="cnrid">
{field => (
<Field>
<FieldLabel htmlFor="cnrid"></FieldLabel>
<Input
id="cnrid"
name="cnrid"
value={field.state.value}
onBlur={field.handleBlur}
onChange={e => field.handleChange(e.target.value)}
/>
<FieldError errors={field.state.meta.errors} />
</Field>
)}
</form.Field>
<DialogFooter>
<form.Subscribe
selector={state => [state.canSubmit, state.isPristine, state.isSubmitting]}
children={([canSubmit, isPristine, isSubmitting]) => (
<Button type="submit" disabled={!canSubmit || isPristine || isSubmitting}>{isSubmitting ? '...' : '开始认证'}</Button>
)}
/>
</DialogFooter>
</form>
);
}
const PassportSchema = z.object({
passportId: z.string().min(9, '护照号应为9个字符').max(9, '护照号应为9个字符'),
});
function PassportForm({ onSubmit }: { onSubmit: OnSubmit }) {
const form = useForm({
defaultValues: {
passportId: '',
},
validators: {
onSubmit: PassportSchema,
},
onSubmit: async (values) => {
await onSubmit({
method: 'passport',
...values.value,
}).catch(() => {
toast('认证失败,请稍后再试');
});
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
void form.handleSubmit();
}}
className="flex flex-col gap-4"
>
<form.Field name="passportId">
{field => (
<Field>
<FieldLabel htmlFor="passportId"></FieldLabel>
<Input
id="passportId"
name="passportId"
value={field.state.value}
onBlur={field.handleBlur}
onChange={e => field.handleChange(e.target.value)}
/>
<FieldError errors={field.state.meta.errors} />
</Field>
)}
</form.Field>
<DialogFooter>
<form.Subscribe
selector={state => [state.canSubmit, state.isPristine, state.isSubmitting]}
children={([canSubmit, isPristine, isSubmitting]) => (
<Button type="submit" disabled={!canSubmit || isPristine || isSubmitting}>{isSubmitting ? '...' : '开始认证'}</Button>
)}
/>
</DialogFooter>
</form>
);
}
type OnSubmit = (submission: KycSubmission) => Promise<void>;
export function KycMethodSelectionDialogView({ onSubmit }: { onSubmit: OnSubmit }) {
const [kycMethod, setKycMethod] = useState<string | null>(null);
return (
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription className="prose">
<p></p>
</DialogDescription>
</DialogHeader>
<Label htmlFor="selection"></Label>
<Select onValueChange={setKycMethod}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="请选择..." />
</SelectTrigger>
<SelectContent>
<SelectGroup id="selection">
<SelectItem value="cnrid"></SelectItem>
<SelectItem value="passport"></SelectItem>
</SelectGroup>
</SelectContent>
</Select>
{kycMethod === 'cnrid' && <CnridForm onSubmit={onSubmit} />}
{kycMethod === 'passport' && <PassportForm onSubmit={onSubmit} />}
</DialogContent>
);
}

View File

@@ -0,0 +1,18 @@
import { HashLoader } from 'react-spinners/esm';
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
export function KycPendingDialogView() {
return (
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
<p>...</p>
<div className="flex justify-center my-12">
<HashLoader color="#e0e0e0" size={100} />
</div>
</DialogDescription>
</DialogHeader>
</DialogContent>
);
}

View File

@@ -0,0 +1,24 @@
import { Button } from '../../ui/button';
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog';
export function KycPromptDialogView({ next }: { next: () => void }) {
return (
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription className="prose">
<p></p>
<p></p>
<ul>
<li> AES-256 </li>
<li></li>
<li> 30 </li>
</ul>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={next}></Button>
</DialogFooter>
</DialogContent>
);
}

View File

@@ -0,0 +1,18 @@
import { Check } from 'lucide-react';
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
export function KycSuccessDialogView() {
return (
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
<p></p>
<div className="flex justify-center my-12">
<Check size={100} />
</div>
</DialogDescription>
</DialogHeader>
</DialogContent>
);
}

View File

@@ -0,0 +1,129 @@
import type { KycSubmission } from './kyc.types';
import { Dialog } from '@radix-ui/react-dialog';
import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect, useState } from 'react';
import { useStore } from 'zustand';
import { postEventJoin, postKycQuery } from '@/client';
import { getEventListInfiniteQueryKey } from '@/client/@tanstack/react-query.gen';
import { useCreateKycSession } from '@/hooks/data/useCreateKycSession';
import { ver } from '@/lib/apiVersion';
import { KycFailedDialogView } from './kyc-failed.dialog.view';
import { KycMethodSelectionDialogView } from './kyc-method-selection.dialog.view';
import { KycPendingDialogView } from './kyc-pending.dialog.view';
import { KycPromptDialogView } from './kyc-prompt.dialog.view';
import { KycSuccessDialogView } from './kyc-success.dialog.view';
import { createKycStore } from './kyc.state';
export function KycDialogContainer({ eventIdToJoin, children }: { eventIdToJoin: string; children: React.ReactNode }) {
const [store] = useState(() => createKycStore(eventIdToJoin));
const isDialogOpen = useStore(store, s => s.isDialogOpen);
const setIsDialogOpen = useStore(store, s => s.setIsDialogOpen);
const stage = useStore(store, s => s.stage);
const setStage = useStore(store, s => s.setStage);
const setKycId = useStore(store, s => s.setKycId);
const { mutateAsync } = useCreateKycSession();
const queryClient = useQueryClient();
const joinEvent = useCallback(async (eventId: string, kycId: string, abortSignal?: AbortSignal) => {
try {
await postEventJoin({
signal: abortSignal,
body: { event_id: eventId, kyc_id: kycId },
headers: ver('20260205'),
});
setStage('success');
}
catch (e) {
console.error('Error joining event:', e);
setStage('failed');
}
}, [setStage]);
const onKycSessionCreate = useCallback(async (submission: KycSubmission) => {
try {
const { data } = await mutateAsync(submission);
setKycId(data!.kyc_id!);
if (data!.status === 'success') {
await joinEvent(eventIdToJoin, data!.kyc_id!, undefined);
}
else if (data!.status === 'processing') {
window.open(data!.redirect_uri, '_blank');
setStage('pending');
}
}
catch (e) {
console.error(e);
setStage('failed');
}
}, [eventIdToJoin, joinEvent, mutateAsync, setKycId, setStage]);
useEffect(() => {
if (stage !== 'pending' || !isDialogOpen) {
return;
}
const controller = new AbortController();
let timer: NodeJS.Timeout;
const poll = async () => {
try {
const { data } = await postKycQuery({
signal: controller.signal,
body: { kyc_id: store.getState().kycId! },
headers: ver('20260205'),
});
const status = data?.data?.status;
if (status === 'success') {
void joinEvent(eventIdToJoin, store.getState().kycId!, controller.signal);
}
else if (status === 'failed') {
setStage('failed');
}
else if (status === 'pending') {
timer = setTimeout(() => void poll(), 1000);
}
else {
// What the fuck?
setStage('failed');
}
}
catch (e) {
if ((e as Error).name === 'AbortError')
return;
console.error('Error fetching KYC status:', e);
setStage('failed');
}
};
void poll();
return () => {
controller.abort();
clearTimeout(timer);
};
}, [stage, store, setStage, isDialogOpen, joinEvent, eventIdToJoin]);
return (
<Dialog
open={isDialogOpen}
onOpenChange={(open) => {
if (!open) {
void queryClient.invalidateQueries({
queryKey: getEventListInfiniteQueryKey({ query: {}, headers: ver('20260205') }),
});
}
setIsDialogOpen(open);
}}
>
{children}
{stage === 'prompt' && <KycPromptDialogView next={() => setStage('methodSelection')} />}
{stage === 'methodSelection' && <KycMethodSelectionDialogView onSubmit={onKycSessionCreate} />}
{stage === 'pending' && <KycPendingDialogView />}
{stage === 'success' && <KycSuccessDialogView />}
{stage === 'failed' && <KycFailedDialogView />}
</Dialog>
);
}

View File

@@ -0,0 +1,34 @@
import { createStore } from 'zustand';
import { devtools } from 'zustand/middleware';
interface KycState {
isDialogOpen: boolean;
eventIdToJoin: string;
kycId: string | null;
stage: 'prompt' | 'methodSelection' | 'pending' | 'success' | 'failed';
setIsDialogOpen: (open: boolean) => void;
setStage: (stage: KycState['stage']) => void;
setKycId: (kycId: string) => void;
}
export function createKycStore(eventIdToJoin: string) {
const initialState = {
isDialogOpen: false,
eventIdToJoin,
kycId: null,
stage: 'prompt' as const,
};
return createStore<KycState>()(devtools(set => ({
...initialState,
setIsDialogOpen: (open: boolean) => set(() =>
open
? { ...initialState, isDialogOpen: true }
: { ...initialState, isDialogOpen: false },
),
setStage: (stage: KycState['stage']) => set(() => ({ stage })),
setKycId: (kycId: string) => set(() => ({ kycId })),
})));
}
export type KycStore = ReturnType<typeof createKycStore>;

View File

@@ -0,0 +1,8 @@
export type KycSubmission = {
method: 'cnrid';
cnrid: string;
name: string;
} | {
method: 'passport';
passportId: string;
};

View File

@@ -1,9 +1,11 @@
export interface EventInfo {
type: 'official' | 'party';
eventId: string;
isJoined: boolean;
requireKyc: boolean;
coverImage: string;
eventName: string;
description: string;
startTime: Date;
endTime: Date;
onJoinEvent: () => void;
}

View File

@@ -30,7 +30,7 @@ const formSchema = z.object({
username: z.string().min(5),
nickname: z.string(),
subtitle: z.string(),
avatar: z.url().or(z.literal('')),
avatar: z.string().url().or(z.literal('')),
allow_public: z.boolean(),
});
export function EditProfileDialogView({ user, updateProfile }: { user: ServiceUserUserInfoData; updateProfile: (data: ServiceUserUserInfoData) => Promise<void> }) {

View File

@@ -0,0 +1,45 @@
import type { KycSubmission } from '@/components/events/kyc/kyc.types';
import { useMutation } from '@tanstack/react-query';
import { postKycSessionMutation } from '@/client/@tanstack/react-query.gen';
import { ver } from '@/lib/apiVersion';
import { utf8ToBase64 } from '@/lib/utils';
type CreateKycSessionBase64Payload = {
// Cnrid
legal_name: string;
resident_id: string;
} | {
// Passport
id: string;
};
export function useCreateKycSession() {
const mutation = useMutation({
...postKycSessionMutation(),
});
return {
...mutation,
mutate: null, // Don't ever use this
mutateAsync: async (data: KycSubmission) => {
const payload = utf8ToBase64(JSON.stringify(
data.method === 'cnrid'
? {
legal_name: data.name,
resident_id: data.cnrid,
} satisfies CreateKycSessionBase64Payload
: {
id: data.passportId,
} satisfies CreateKycSessionBase64Payload,
));
const response = await mutation.mutateAsync({
body: {
identity: payload,
type: data.method,
},
headers: ver('20260205'),
});
return response;
},
};
}

View File

@@ -1,19 +1,19 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { isNil } from 'lodash-es';
import { getEventListInfiniteOptions } from '@/client/@tanstack/react-query.gen';
const LIMIT = 12;
import { ver } from '@/lib/apiVersion';
export function useGetEvents() {
return useInfiniteQuery({
...getEventListInfiniteOptions({
query: { limit: String(LIMIT), offset: String(0) },
query: {},
headers: ver('20260205'),
}),
initialPageParam: '0',
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
const currentData = lastPage?.data;
if (!isNil(currentData) && currentData.length === LIMIT) {
return String(allPages.length * LIMIT);
if (!isNil(currentData) && currentData.length === 20) {
return allPages.length * 20;
}
return undefined;
},

View File

@@ -1,8 +0,0 @@
import { useMutation } from '@tanstack/react-query';
import { postEventJoinMutation } from '@/client/@tanstack/react-query.gen';
export function useJoinEvent() {
return useMutation({
...postEventJoinMutation(),
});
}

View File

@@ -1,16 +1,17 @@
import type { ServiceUserUserInfoData } from '@/client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { getUserInfoByUserIdQueryKey, getUserInfoQueryKey, patchUserUpdateMutation } from '@/client/@tanstack/react-query.gen';
import { ver } from '@/lib/apiVersion';
export function useUpdateUser() {
const queryClient = useQueryClient();
const data: { data: ServiceUserUserInfoData | undefined } | undefined = queryClient.getQueryData(getUserInfoQueryKey());
const data: { data: ServiceUserUserInfoData | undefined } | undefined = queryClient.getQueryData(getUserInfoQueryKey({ headers: ver('20260205') }));
return useMutation({
...patchUserUpdateMutation(),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: getUserInfoQueryKey() });
await queryClient.invalidateQueries({ queryKey: getUserInfoQueryKey({ headers: ver('20260205') }) });
if ((data?.data?.user_id) != null) {
await queryClient.invalidateQueries({ queryKey: getUserInfoByUserIdQueryKey({ path: { user_id: data.data.user_id } }) });
await queryClient.invalidateQueries({ queryKey: getUserInfoByUserIdQueryKey({ path: { user_id: data.data.user_id }, headers: ver('20260205') }) });
}
},
});

View File

@@ -3,17 +3,18 @@ import {
getUserInfoByUserIdOptions,
getUserInfoOptions,
} from '@/client/@tanstack/react-query.gen';
import { ver } from '@/lib/apiVersion';
export function useUserInfo() {
return useSuspenseQuery({
...getUserInfoOptions(),
...getUserInfoOptions({ headers: ver('20260205') }),
staleTime: 10 * 60 * 1000,
});
}
export function useOtherUserInfo(userId: string) {
return useSuspenseQuery({
...getUserInfoByUserIdOptions({ path: { user_id: userId } }),
...getUserInfoByUserIdOptions({ path: { user_id: userId }, headers: ver('20260205') }),
staleTime: 10 * 60 * 1000,
retry: (_failureCount, error) => error.code !== 403,
});

View File

@@ -0,0 +1,5 @@
export function ver(version: string) {
return {
'X-Api-Version': version,
};
}

View File

@@ -1,3 +1,4 @@
import type { Query } from '@tanstack/react-query';
import type { ClassValue } from 'clsx';
// eslint-disable-next-line unicorn/prefer-node-protocol
import { Buffer } from 'buffer';
@@ -17,3 +18,12 @@ export function base64ToUtf8(base64: string): string {
export function utf8ToBase64(utf8: string): string {
return Buffer.from(utf8, 'utf-8').toString('base64');
}
export function invalidateBlurry(id: string) {
return {
predicate: (query: Query<unknown, Error, unknown, readonly unknown[]>) => {
const key = query.queryKey[0] as { _id: string };
return key?._id === id;
},
};
}

View File

@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { EventCardSkeleton } from '@/components/events/event-card.skeleton';
import { EventCardView } from '@/components/events/event-card.view';
import { Button } from '@/components/ui/button';
const meta = {
title: 'Events/EventCard',
@@ -12,25 +13,35 @@ type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
type: 'official',
coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-watersplash.png?raw=true',
eventName: 'Nix CN Conference 26.05',
description: 'Event Description',
startTime: new Date('2026-06-13T04:00:00.000Z'),
endTime: new Date('2026-06-14T04:00:00.000Z'),
onJoinEvent: () => { },
eventInfo: {
eventId: '1',
type: 'official',
requireKyc: true,
isJoined: false,
coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-watersplash.png?raw=true',
eventName: 'Nix CN Conference 26.05',
description: 'Event Description',
startTime: new Date('2026-06-13T04:00:00.000Z'),
endTime: new Date('2026-06-14T04:00:00.000Z'),
},
actionFooter: <Button className="w-full"></Button>,
},
};
export const Loading: Story = {
render: () => <EventCardSkeleton />,
args: {
type: 'official',
coverImage: '',
eventName: '',
description: '',
startTime: new Date(0),
endTime: new Date(0),
onJoinEvent: () => { },
eventInfo: {
eventId: '1',
type: 'official',
requireKyc: true,
coverImage: '',
isJoined: false,
eventName: '',
description: '',
startTime: new Date(0),
endTime: new Date(0),
},
actionFooter: <Button className="w-full"></Button>,
},
};

View File

@@ -1,6 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { EventGridSkeleton } from '@/components/events/event-grid.skeleton';
import { EventGridView } from '@/components/events/event-grid.view';
import { Button } from '@/components/ui/button';
import { Skeleton as UiSkeleton } from '@/components/ui/skeleton';
const meta = {
title: 'Events/EventGrid',
@@ -14,42 +16,51 @@ export const Primary: Story = {
args: {
events: [
{
eventId: '1',
requireKyc: true,
isJoined: false,
type: 'official',
coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-watersplash.png?raw=true',
eventName: 'Nix CN Conference 26.05',
description: 'Event Description',
startTime: new Date('2026-06-13T04:00:00.000Z'),
endTime: new Date('2026-06-14T04:00:00.000Z'),
onJoinEvent: () => { },
},
{
eventId: '2',
requireKyc: true,
isJoined: false,
type: 'official',
coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-moonscape.png?raw=true',
eventName: 'Nix CN Conference 26.05',
description: 'Event Description',
startTime: new Date('2026-06-13T04:00:00.000Z'),
endTime: new Date('2026-06-14T04:00:00.000Z'),
onJoinEvent: () => { },
},
{
eventId: '3',
requireKyc: true,
isJoined: false,
type: 'official',
coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-nineish-catppuccin-latte.png?raw=true',
eventName: 'Nix CN Conference 26.05',
description: 'Event Description',
startTime: new Date('2026-06-13T04:00:00.000Z'),
endTime: new Date('2026-06-14T04:00:00.000Z'),
onJoinEvent: () => { },
},
{
eventId: '4',
requireKyc: true,
isJoined: false,
type: 'official',
coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nixos-wallpaper-catppuccin-macchiato.png?raw=true',
eventName: 'Nix CN Conference 26.05',
description: 'Event Description',
startTime: new Date('2026-06-13T04:00:00.000Z'),
endTime: new Date('2026-06-14T04:00:00.000Z'),
onJoinEvent: () => { },
},
],
assembleFooter: () => <Button className="w-full"></Button>,
},
};
@@ -57,5 +68,6 @@ export const Skeleton: Story = {
render: () => <EventGridSkeleton />,
args: {
events: [],
assembleFooter: () => <UiSkeleton className="w-full" />,
},
};

View File

@@ -0,0 +1,51 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { KycFailedDialogView } from '@/components/events/kyc/kyc-failed.dialog.view';
import { KycMethodSelectionDialogView } from '@/components/events/kyc/kyc-method-selection.dialog.view';
import { KycPendingDialogView } from '@/components/events/kyc/kyc-pending.dialog.view';
import { KycPromptDialogView } from '@/components/events/kyc/kyc-prompt.dialog.view';
import { KycSuccessDialogView } from '@/components/events/kyc/kyc-success.dialog.view';
import { Dialog } from '@/components/ui/dialog';
const meta = {
title: 'Events/KycDialog',
component: KycPromptDialogView,
decorators: [
Story => (
<Dialog open={true}>
<Story />
</Dialog>
),
],
} satisfies Meta<typeof KycPromptDialogView>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Prompt: Story = {
args: {
},
};
export const MethodSelection: Story = {
render: () => <KycMethodSelectionDialogView onSubmit={async () => Promise.resolve()} />,
args: {
},
};
export const Pending: Story = {
render: () => <KycPendingDialogView />,
args: {
},
};
export const Success: Story = {
render: () => <KycSuccessDialogView />,
args: {
},
};
export const Failed: Story = {
render: () => <KycFailedDialogView />,
args: {
},
};