Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ VITE_ANALYTICS_SUBDOMAIN_URL=https://metrics.string.xyz

VITE_IPFS_GATEWAY=https://nftstorage.link/ipfs/
VITE_IPFS_CID=bafybeieqi56p6vlxofj6wkoort2m5r72ajhtikpzo53wnyze5isvn34fze

VITE_STRING_API_KEY=str...

64 changes: 64 additions & 0 deletions src/lib/services/apiClient.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ export function createApiClient({ baseUrl, apiKey }: ApiClientOptions): ApiClien
}
}

async function requestDeviceVerification(nonce: string, signature: string, visitor: VisitorData) {
const body = {
nonce,
signature,
fingerprint: visitor,
};

try {
await httpClient.post<void>(`/users/verify-device`, body, {
headers: authHeaders,
});
} catch (e: any) {
const error = _getErrorFromAxiosError(e);
throw error;
}
}

async function loginUser(nonce: string, signature: string, visitor?: VisitorData, bypassDeviceCheck = false) {
const body = {
nonce,
Expand Down Expand Up @@ -144,6 +161,47 @@ export function createApiClient({ baseUrl, apiKey }: ApiClientOptions): ApiClien
}
}

async function getDeviceStatus(nonce: string, signature: string, visitor: VisitorData) {
const body = {
nonce,
signature,
fingerprint: visitor,
};

try {
const { data } = await httpClient.post<{ status: string }>(`/users/device-status`, body, {
headers: authHeaders,
});

return data;
} catch (e: any) {
const error = _getErrorFromAxiosError(e);
throw error;
}
}

async function getUserEmailPreview(nonce: string, signature: string) {
// The request body takes a fingerprint but it does not matter what it is
const body = {
nonce,
signature,
fingerprint: {
visitorId: "",
requestId: "",
}
}
try {
const { data } = await httpClient.post<{email: string}>(`/users/preview-email`, body, {
headers: authHeaders,
});

return data;
} catch (e: any) {
const error = _getErrorFromAxiosError(e);
throw error;
}
}

async function getQuote(payload: TransactionRequest) {
try {
const request = () => httpClient.post(`/quotes`, payload);
Expand Down Expand Up @@ -205,10 +263,13 @@ export function createApiClient({ baseUrl, apiKey }: ApiClientOptions): ApiClien
createUser,
updateUser,
requestEmailVerification,
requestDeviceVerification,
loginUser,
refreshToken,
logoutUser,
getUserStatus,
getDeviceStatus,
getUserEmailPreview,
getQuote,
transact,
setWalletAddress,
Expand All @@ -220,10 +281,13 @@ export interface ApiClient {
createUser: (nonce: string, signature: string, visitor?: VisitorData) => Promise<AuthResponse>;
updateUser: (userId: string, userUpdate: UserUpdate) => Promise<User>;
requestEmailVerification: (userId: string, email: string) => Promise<void>;
requestDeviceVerification: (nonce: string, signature: string, visitor: VisitorData) => Promise<void>;
loginUser: (nonce: string, signature: string, visitor?: VisitorData, bypassDeviceCheck?: boolean) => Promise<AuthResponse>;
refreshToken: (walletAddress: string) => Promise<AuthResponse>;
logoutUser: () => Promise<void>;
getUserStatus: (userId: string) => Promise<{ status: string }>;
getDeviceStatus: (nonce: string, signature: string, visitor: VisitorData) => Promise<{ status: string }>;
getUserEmailPreview: (nonce: string, signature: string) => Promise<{ email: string }>;
getQuote: (request: TransactionRequest) => Promise<Quote>;
transact: (request: ExecutionRequest) => Promise<TransactionResponse>;
setWalletAddress: (walletAddress: string) => void;
Expand Down
13 changes: 9 additions & 4 deletions src/lib/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,16 @@ export function createAuthService({ apiClient, locationService, bypassDeviceChec
}
}

const retryLogin = async () => {
const getPreviousSignature = async () => {
// TODO: Use refresh token instead
// TODO: Modify refresh token endpoint to verify visitor data
if (!previousAttempt.signature) throw { code: "UNAUTHORIZED" };

const visitorData = await locationService.getVisitorData();
return login(previousAttempt.nonce, previousAttempt.signature, visitorData);

if (!visitorData) throw new Error("cannot get device data");

return { nonce: previousAttempt.nonce, signature: previousAttempt.signature, visitor: visitorData}
};


Expand Down Expand Up @@ -78,7 +81,8 @@ export function createAuthService({ apiClient, locationService, bypassDeviceChec
return {
loginOrCreateUser,
fetchLoggedInUser,
retryLogin,
requestSignature,
getPreviousSignature,
logout
};
}
Expand All @@ -92,6 +96,7 @@ export interface AuthServiceParams {
export interface AuthService {
loginOrCreateUser: (walletAddress: string) => Promise<{ user: User }>;
fetchLoggedInUser: (walletAddress: string) => Promise<User | null>;
retryLogin: () => Promise<{ user: User }>;
requestSignature: (userAddress: string, encodedMessage: string) => Promise<string>;
getPreviousSignature: () => Promise<{nonce: string, signature: string, visitor: VisitorData}>;
logout: () => Promise<any>;
}
138 changes: 99 additions & 39 deletions src/lib/services/events.service.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
import type { StringPay, StringPayload } from "../StringPay";
import type { ApiClient, ExecutionRequest, TransactionRequest, Quote, PaymentInfo, User, UserUpdate } from "./apiClient.service";
import type { AuthService } from "./auth.service";
import type { LocationService } from "./location.service";
import type { QuoteService } from "./quote.service";
import type { AuthService } from "./auth.service";

const CHANNEL = "STRING_PAY";

export enum Events {
LOAD_PAYLOAD = "load_payload",
IFRAME_READY = "ready",
IFRAME_RESIZE = "resize",
IFRAME_CLOSE = "close",
REQUEST_AUTHORIZE_USER = "request_authorize_user",
RECEIVE_AUTHORIZE_USER = "receive_authorize_user",
REQUEST_RETRY_LOGIN = "request_retry_login",
RECEIVE_RETRY_LOGIN = "receive_retry_login",
REQUEST_UPDATE_USER = 'request_update_user',
RECEIVE_UPDATE_USER = 'receive_update_user',
REQUEST_EMAIL_VERIFICATION = "request_email_verification",
RECEIVE_EMAIL_VERIFICATION = "receive_email_verification",
LOAD_PAYLOAD = "load_payload",
IFRAME_READY = "ready",
IFRAME_RESIZE = "resize",
IFRAME_CLOSE = "close",
REQUEST_AUTHORIZE_USER = "request_authorize_user",
RECEIVE_AUTHORIZE_USER = "receive_authorize_user",
REQUEST_UPDATE_USER = 'request_update_user',
RECEIVE_UPDATE_USER = 'receive_update_user',
REQUEST_EMAIL_VERIFICATION = "request_email_verification",
RECEIVE_EMAIL_VERIFICATION = "receive_email_verification",
REQUEST_EMAIL_PREVIEW = "request_email_preview",
RECEIVE_EMAIL_PREVIEW = "receive_email_preview",
REQUEST_DEVICE_VERIFICATION = "request_device_verification",
RECEIVE_DEVICE_VERIFICATION = "receive_device_verification",
REQUEST_CONFIRM_TRANSACTION = "request_confirm_transaction",
RECEIVE_CONFIRM_TRANSACTION = "receive_confirm_transaction",
REQUEST_QUOTE_START = "request_quote_start",
QUOTE_CHANGED = "quote_changed",
REQUEST_QUOTE_STOP = "request_quote_stop",
REQUEST_QUOTE_START = "request_quote_start",
QUOTE_CHANGED = "quote_changed",
REQUEST_QUOTE_STOP = "request_quote_stop",
}

const eventHandlers: Record<string, (event: StringEvent, stringPay: StringPay) => void> = {};

export function createEventsService(iframeUrl: string, authService: AuthService, quoteService: QuoteService, apiClient: ApiClient, locationService: LocationService) {
let emailCheckInterval: NodeJS.Timer | undefined;
let deviceCheckInterval: NodeJS.Timer | undefined;

const sendEvent = <T = any>(frame: HTMLIFrameElement, eventName: string, data?: T, error?: any) => {
if (!frame) {
err("a frame was not provided to sendEvent");
Expand Down Expand Up @@ -72,6 +77,10 @@ export function createEventsService(iframeUrl: string, authService: AuthService,

function cleanup() {
unregisterEvents();

clearInterval(emailCheckInterval);
clearInterval(deviceCheckInterval);

const stringPay = window.StringPay;

if (stringPay) {
Expand All @@ -82,16 +91,17 @@ export function createEventsService(iframeUrl: string, authService: AuthService,
}
}

//
eventHandlers[Events.IFRAME_READY] = onIframeReady;
eventHandlers[Events.IFRAME_CLOSE] = onIframeClose;
eventHandlers[Events.IFRAME_RESIZE] = onIframeResize;
eventHandlers[Events.REQUEST_AUTHORIZE_USER] = onAuthorizeUser;
eventHandlers[Events.REQUEST_RETRY_LOGIN] = onRetryLogin;
eventHandlers[Events.REQUEST_UPDATE_USER] = onUpdateUser;
eventHandlers[Events.REQUEST_EMAIL_VERIFICATION] = onEmailVerification;
eventHandlers[Events.REQUEST_QUOTE_START] = onQuoteStart;
eventHandlers[Events.REQUEST_QUOTE_STOP] = onQuoteStop;
// Hook event handlers to the events
eventHandlers[Events.IFRAME_READY] = onIframeReady;
eventHandlers[Events.IFRAME_CLOSE] = onIframeClose;
eventHandlers[Events.IFRAME_RESIZE] = onIframeResize;
eventHandlers[Events.REQUEST_AUTHORIZE_USER] = onAuthorizeUser;
eventHandlers[Events.REQUEST_UPDATE_USER] = onUpdateUser;
eventHandlers[Events.REQUEST_EMAIL_VERIFICATION] = onEmailVerification;
eventHandlers[Events.REQUEST_EMAIL_PREVIEW] = onEmailPreview;
eventHandlers[Events.REQUEST_DEVICE_VERIFICATION] = onDeviceVerification;
eventHandlers[Events.REQUEST_QUOTE_START] = onQuoteStart;
eventHandlers[Events.REQUEST_QUOTE_STOP] = onQuoteStop;
eventHandlers[Events.REQUEST_CONFIRM_TRANSACTION] = onConfirmTransaction;

/** -------------- EVENT HANDLERS ---------------- */
Expand Down Expand Up @@ -147,17 +157,6 @@ export function createEventsService(iframeUrl: string, authService: AuthService,
}
}

async function onRetryLogin(event: StringEvent, { frame }: StringPay) {
if (!frame) throw new Error("Iframe not ready");

try {
const { user } = await authService.retryLogin();
sendEvent(frame, Events.RECEIVE_RETRY_LOGIN, { user });
} catch (error) {
sendEvent(frame, Events.RECEIVE_RETRY_LOGIN, {}, error);
}
}

async function onUpdateUser(event: StringEvent, { frame }: StringPay) {
if (!frame) throw new Error("Iframe not ready");

Expand All @@ -178,18 +177,79 @@ export function createEventsService(iframeUrl: string, authService: AuthService,
const data = <{ userId: string; email: string }>event.data;

await apiClient.requestEmailVerification(data.userId, data.email);
const check = setInterval(async () => {

clearInterval(emailCheckInterval);
emailCheckInterval = setInterval(async () => {
const { status } = await apiClient.getUserStatus(data.userId);
if (status == "email_verified") {
sendEvent(frame, Events.RECEIVE_EMAIL_VERIFICATION, { status });
clearInterval(check);
clearInterval(emailCheckInterval);
}
}, 5000);
} catch (error: any) {
sendEvent(frame, Events.RECEIVE_EMAIL_VERIFICATION, {}, error);
}
}

async function onDeviceVerification(event: StringEvent, { frame }: StringPay) {
if (!frame) throw new Error("Iframe not ready");

try {
const data = <{ walletAddress: string }>event.data;

let nonce: string;
let signature: string;

const previous = await authService.getPreviousSignature();
nonce = previous.nonce;
signature = previous.signature;

if (!previous.signature) {
nonce = (await apiClient.requestLogin(data.walletAddress)).nonce;
signature = await authService.requestSignature(data.walletAddress, nonce);
}

await apiClient.requestDeviceVerification(nonce, signature, previous.visitor);

clearInterval(deviceCheckInterval);
deviceCheckInterval = setInterval(async () => {
const { status } = await apiClient.getDeviceStatus(nonce, signature, previous.visitor);
if (status == "verified") {
sendEvent(frame, Events.RECEIVE_DEVICE_VERIFICATION, { status });
clearInterval(deviceCheckInterval);
}
}, 5000);
} catch (error: any) {
sendEvent(frame, Events.RECEIVE_DEVICE_VERIFICATION, {}, error);
}
}

async function onEmailPreview(event: StringEvent, { frame }: StringPay) {
if (!frame) throw new Error("Iframe not ready");

try {
const data = <{ walletAddress: string }>event.data;

let nonce: string;
let signature: string;

const previous = await authService.getPreviousSignature();
nonce = previous.nonce;
signature = previous.signature;

if (!previous.signature) {
nonce = (await apiClient.requestLogin(data.walletAddress)).nonce;
signature = await authService.requestSignature(data.walletAddress, nonce);
}

const { email } = await apiClient.getUserEmailPreview(nonce, signature);

sendEvent(frame, Events.RECEIVE_EMAIL_PREVIEW, { email });
} catch (error: any) {
sendEvent(frame, Events.RECEIVE_EMAIL_PREVIEW, {}, error);
}
}

async function onQuoteStart(event: StringEvent, { frame, payload }: StringPay) {
if (!frame) throw new Error("Iframe not ready");

Expand Down