diff --git a/.env.example b/.env.example index 06c04d4..b250edc 100644 --- a/.env.example +++ b/.env.example @@ -1,21 +1,20 @@ #### PRODUCTION -# VITE_IFRAME_URL=https://iframe.string-api.xyz -# VITE_API_URL=https://api.string-api.xyz +PROD_IFRAME_URL=https://iframe.string-api.xyz +PROD_API_URL=https://api.string-api.xyz #### DEVELOPMENT -# VITE_IFRAME_URL=https://iframe-app.dev.string-api.xyz -# VITE_API_URL=https://string-api.dev.string-api.xyz +DEV_IFRAME_URL=https://iframe-app.dev.string-api.xyz +DEV_API_URL=https://string-api.dev.string-api.xyz #### SANDBOX -# VITE_IFRAME_URL=https://iframe-app.dev.string-api.xyz -# VITE_API_URL=https://api.sandbox.string-api.xyz +SBOX_IFRAME_URL=https://iframe-app.dev.string-api.xyz +SBOX_API_URL=https://api.sandbox.string-api.xyz #### LOCAL -VITE_IFRAME_URL=http://localhost:4040 -VITE_API_URL=http://localhost:5555 +LOCAL_IFRAME_URL=http://localhost:4040 +LOCAL_API_URL=http://localhost:5555 -VITE_STRING_API_KEY=str... - -#### ANALYTICS VITE_ANALYTICS_LIB_PK= -VITE_ANALYTICS_SUBDOMAIN_URL=https://metrics.string.xyz \ No newline at end of file +VITE_ANALYTICS_SUBDOMAIN_URL=https://metrics.string.xyz + +VITE_STRING_API_KEY=str... diff --git a/rollup.config.js b/rollup.config.js index 6944650..4fc40b2 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -11,8 +11,29 @@ config(); const version = process.env.npm_package_version -if (!process.env.VITE_IFRAME_URL || !process.env.VITE_API_URL || !process.env.VITE_ANALYTICS_LIB_PK) { - throw Error("Missing variables in .env") +const ENV_TABLE = [ + { name: 'PROD_IFRAME_URL', value: process.env.PROD_IFRAME_URL }, + { name: 'PROD_API_URL', value: process.env.PROD_API_URL }, + { name: 'SBOX_IFRAME_URL', value: process.env.SBOX_IFRAME_URL }, + { name: 'SBOX_API_URL', value: process.env.SBOX_API_URL }, + { name: 'DEV_IFRAME_URL', value: process.env.DEV_IFRAME_URL }, + { name: 'DEV_API_URL', value: process.env.DEV_API_URL }, + { name: 'LOCAL_IFRAME_URL', value: process.env.LOCAL_IFRAME_URL }, + { name: 'LOCAL_API_URL', value: process.env.LOCAL_API_URL }, +] + +for (const env of ENV_TABLE) { + if (!env.value) { + throw Error(`Missing ${env.name} in .env`) + } +} + +if (!process.env.VITE_ANALYTICS_LIB_PK) { + throw Error(`Missing VITE_ANALYTICS_LIB_PK in .env`) +} + +if (!process.env.VITE_ANALYTICS_SUBDOMAIN_URL) { + throw Error(`Missing VITE_ANALYTICS_SUBDOMAIN_URL in .env`) } export default { @@ -30,9 +51,12 @@ export default { resolve({ jsnext: true, preferBuiltins: true, browser: true }), replace({ values: { - 'import.meta.env.VITE_IFRAME_URL': JSON.stringify(new URL(process.env.VITE_IFRAME_URL).origin), - 'import.meta.env.VITE_API_URL': JSON.stringify(new URL(process.env.VITE_API_URL).origin), + ...ENV_TABLE.reduce((acc, env) => { + acc[`import.meta.env.${env.name}`] = JSON.stringify(new URL(env.value).origin); + return acc + }, {}), 'import.meta.env.VITE_ANALYTICS_LIB_PK': JSON.stringify(process.env.VITE_ANALYTICS_LIB_PK), + 'import.meta.env.VITE_ANALYTICS_SUBDOMAIN_URL': JSON.stringify(process.env.VITE_ANALYTICS_SUBDOMAIN_URL), }, preventAssignment: true }), diff --git a/src/lib/StringPay.d.ts b/src/lib/StringPay.d.ts index c615dab..3bd9086 100644 --- a/src/lib/StringPay.d.ts +++ b/src/lib/StringPay.d.ts @@ -1,6 +1,4 @@ export interface StringPayload { - options?: StringOptions; - apiKey: string; name: string; collection?: string; currency: string; @@ -17,16 +15,21 @@ export interface StringPayload { gasLimit?: string; } +export type StringSDKEnvironment = "PROD" | "SANDBOX" | "DEV" | "LOCAL"; + export interface StringOptions { + env: StringSDKEnvironment; + publicKey: string; bypassDeviceCheck?: boolean; } export declare class StringPay { - container?: Element; - frame?: HTMLIFrameElement; - payload?: StringPayload; isLoaded: boolean; + payload?: StringPayload; + frame?: HTMLIFrameElement; + container?: Element; onFrameLoad: () => void; onFrameClose: () => void; + init(options: StringOptions): void; loadFrame(payload: StringPayload): void; } diff --git a/src/lib/StringPay.ts b/src/lib/StringPay.ts index 20fc3f0..5f786ba 100644 --- a/src/lib/StringPay.ts +++ b/src/lib/StringPay.ts @@ -1,8 +1,6 @@ import { createServices, type Services } from "./services"; export interface StringPayload { - options?: StringOptions - apiKey: string; name: string; collection?: string; currency: string; @@ -19,32 +17,71 @@ export interface StringPayload { gasLimit?: string; } +export type StringSDKEnvironment = "PROD" | "SANDBOX" | "DEV" | "LOCAL"; + export interface StringOptions { + env: StringSDKEnvironment; + publicKey: string; bypassDeviceCheck?: boolean; } -const IFRAME_URL = import.meta.env.VITE_IFRAME_URL; -const API_URL = import.meta.env.VITE_API_URL; +type StringEnvDetails = { + IFRAME_URL: string; + API_URL: string; +} + +const ENV_TABLE: Record = { + "PROD": { + IFRAME_URL: import.meta.env.PROD_IFRAME_URL, + API_URL: import.meta.env.PROD_API_URL, + }, + "SANDBOX": { + IFRAME_URL: import.meta.env.SBOX_IFRAME_URL, + API_URL: import.meta.env.SBOX_API_URL, + }, + "DEV": { + IFRAME_URL: import.meta.env.DEV_IFRAME_URL, + API_URL: import.meta.env.DEV_API_URL, + }, + "LOCAL": { + IFRAME_URL: import.meta.env.LOCAL_IFRAME_URL, + API_URL: import.meta.env.LOCAL_API_URL, + } +} const err = (msg: string) => { console.error("[String Pay] " + msg); }; export class StringPay { - container?: Element; - frame?: HTMLIFrameElement; - payload?: StringPayload; isLoaded = false; - services: Services; - private _loadIframeCallback = () => {}; - - constructor(loadIframeCallback: () => void) { - this._loadIframeCallback = loadIframeCallback; - } + payload?: StringPayload; + frame?: HTMLIFrameElement; + container?: Element; + #services: Services; + private _IFRAME_URL: string; onFrameLoad = () => {}; onFrameClose = () => {}; + init(options: StringOptions) { + const envDetails = ENV_TABLE[options.env]; + if (!envDetails) { + return err(`Invalid environment: ${options.env}`); + } + + if (!options.publicKey) return err("You need an API key to use the String SDK"); + if (options.publicKey.slice(0, 4) !== "str.") return err(`Invalid API Key: ${options.publicKey}`); + + this._IFRAME_URL = envDetails.IFRAME_URL; + this.#services = createServices({ + baseUrl: envDetails.API_URL, + iframeUrl: envDetails.IFRAME_URL, + apiKey: options.publicKey, + bypassDeviceCheck: options.bypassDeviceCheck ?? false + }); + } + async loadFrame(payload: StringPayload) { // make sure there is a wallet connected if (!window.ethereum || !window.ethereum.selectedAddress) return err("No wallet connected, please connect wallet"); @@ -59,10 +96,9 @@ export class StringPay { // Validate payload if (!payload) return err("No payload specified"); - if (!payload.apiKey) return err("You must have an api key in your payload"); - if (payload.apiKey.slice(0, 4) !== "str.") return err(`Invalid API Key: ${payload.apiKey}`); if (!payload.userAddress) return err("No user address found, please connect wallet"); - if (!IFRAME_URL) return err("IFRAME_URL not specified"); + if (!this._IFRAME_URL) return err("IFRAME_URL not specified. Did you call init()?"); + if (!this.#services) return err("Services not initialized. Did you call init()?"); // Set payload this.payload = payload; @@ -72,7 +108,8 @@ export class StringPay { iframe.style.width = "100vh"; iframe.style.height = "700px"; iframe.style.overflow = "none"; - iframe.src = IFRAME_URL; + iframe.src = this._IFRAME_URL; + container.appendChild(iframe); this.container = container; this.frame = iframe; @@ -80,26 +117,15 @@ export class StringPay { // set the default gas limit this.payload.gasLimit = "8000000"; // TODO: Do we want this value to change dynamically? - this._loadIframeCallback(); + this.#services.eventsService.unregisterEvents(); + this.#services.eventsService.registerEvents(); } } function main() { - // This is the starting point of the library - // We create services that contain all the logic for the library - // StringPay is the main class that is exported to the user - // We also create a callback that is called when the iframe is loaded. This is used to register events - - const services = createServices({ - apiUrl: API_URL, - }); - - const loadIframeCallback = () => { - services.eventsService.unregisterEvents(); - services.eventsService.registerEvents(); - }; + // Expose the StringPay instance to the window - const stringPay = new StringPay(loadIframeCallback); + const stringPay = new StringPay(); (window).StringPay = stringPay; } diff --git a/src/lib/services/apiClient.service.ts b/src/lib/services/apiClient.service.ts index 0d02e16..b5bc2a5 100644 --- a/src/lib/services/apiClient.service.ts +++ b/src/lib/services/apiClient.service.ts @@ -2,9 +2,8 @@ import axios from "redaxios"; // TODO: Fix timeout issue -export function createApiClient({ baseUrl }: ApiClientOptions): ApiClient { +export function createApiClient({ baseUrl, apiKey }: ApiClientOptions): ApiClient { let _userWalletAddress = ""; - let apiKey = ""; const commonHeaders: any = { "Content-Type": "application/json", @@ -18,7 +17,6 @@ export function createApiClient({ baseUrl }: ApiClientOptions): ApiClient { }); const setWalletAddress = (addr: string) => (_userWalletAddress = addr); - const setApiKey = (key: string) => (apiKey = key); async function createApiKey() { const { data } = await httpClient.post<{ apiKey: string }>("/apikeys"); @@ -230,8 +228,7 @@ export function createApiClient({ baseUrl }: ApiClientOptions): ApiClient { getUserStatus, getQuote, transact, - setWalletAddress, - setApiKey, + setWalletAddress }; } @@ -250,7 +247,6 @@ export interface ApiClient { getQuote: (payload: QuoteRequestPayload) => Promise; transact: (quote: TransactPayload) => Promise; setWalletAddress: (walletAddress: string) => void; - setApiKey: (apiKey: string) => void; } interface ApiKeyResponse { @@ -339,4 +335,5 @@ export interface QuoteRequestPayload { export interface ApiClientOptions { baseUrl: string; + apiKey: string; } diff --git a/src/lib/services/auth.service.ts b/src/lib/services/auth.service.ts index 169a007..a6cad1d 100644 --- a/src/lib/services/auth.service.ts +++ b/src/lib/services/auth.service.ts @@ -1,17 +1,11 @@ import type { ApiClient, User } from './apiClient.service'; import type { LocationService, VisitorData } from './location.service'; -export function createAuthService({ apiClient, locationService }: { apiClient: ApiClient, locationService: LocationService }): AuthService { +export function createAuthService({ apiClient, locationService, bypassDeviceCheck }: AuthServiceParams): AuthService { const previousAttempt = { signature: "", nonce: "" }; - let _bypassDeviceCheck = false; - - const setBypassDeviceCheck = (value?: boolean) => { - _bypassDeviceCheck = value || false; - }; - const login = async (nonce: string, signature: string, visitorData?: VisitorData) => { - const data = await apiClient.loginUser(nonce, signature, visitorData, _bypassDeviceCheck); + const data = await apiClient.loginUser(nonce, signature, visitorData, bypassDeviceCheck); return data; }; @@ -85,15 +79,19 @@ export function createAuthService({ apiClient, locationService }: { apiClient: A loginOrCreateUser, fetchLoggedInUser, retryLogin, - logout, - setBypassDeviceCheck + logout }; } +export interface AuthServiceParams { + apiClient: ApiClient; + locationService: LocationService; + bypassDeviceCheck: boolean; +} + export interface AuthService { loginOrCreateUser: (walletAddress: string) => Promise<{ user: User }>; fetchLoggedInUser: (walletAddress: string) => Promise; retryLogin: () => Promise<{ user: User }>; logout: () => Promise; - setBypassDeviceCheck: (value?: boolean) => void; } \ No newline at end of file diff --git a/src/lib/services/events.service.ts b/src/lib/services/events.service.ts index adfbb14..c1892e0 100644 --- a/src/lib/services/events.service.ts +++ b/src/lib/services/events.service.ts @@ -5,7 +5,6 @@ import type { LocationService } from "./location.service"; import type { QuoteService } from "./quote.service"; const CHANNEL = "STRING_PAY"; -const IFRAME_URL = new URL(import.meta.env.VITE_IFRAME_URL).origin; export enum Events { LOAD_PAYLOAD = "load_payload", @@ -29,47 +28,7 @@ export enum Events { const eventHandlers: Record void> = {}; -const _handleEvent = async (e: any) => { - if (e.origin !== IFRAME_URL) return; - - const stringPay: StringPay = (window).StringPay; - - try { - const payload = JSON.parse(e.data); - const channel = payload.channel; - const event = payload.event; - if (channel === CHANNEL) { - const handler = eventHandlers[event.eventName]; - if (handler) await handler(event, stringPay); - else console.debug("SDK :: Unhandled event: ", event); - } - } catch (error) { - console.error("sdk: _handleEvent error: ", error); - } -}; - -const registerEvents = () => { - unregisterEvents(); - - window.addEventListener("message", _handleEvent); -}; - -const unregisterEvents = () => { - window.removeEventListener("message", _handleEvent); -}; - -function cleanup() { - unregisterEvents(); - - if ((window).StringPay) { - (window).StringPay.frame?.remove(); - (window).StringPay.frame = undefined; - (window).StringPay.isLoaded = false; - (window).StringPay.onFrameClose(); - } -} - -export function createEventsService(authService: AuthService, quoteService: QuoteService, apiClient: ApiClient, locationService: LocationService) { +export function createEventsService(iframeUrl: string, authService: AuthService, quoteService: QuoteService, apiClient: ApiClient, locationService: LocationService) { const sendEvent = (frame: HTMLIFrameElement, eventName: string, data?: T, error?: any) => { if (!frame) { err("a frame was not provided to sendEvent"); @@ -84,6 +43,46 @@ export function createEventsService(authService: AuthService, quoteService: Quot frame.contentWindow?.postMessage(message, "*"); }; + const _handleEvent = async (e: any) => { + if (e.origin !== iframeUrl) return; + + const stringPay: StringPay = (window).StringPay; + + try { + const payload = JSON.parse(e.data); + const channel = payload.channel; + const event = payload.event; + if (channel === CHANNEL) { + const handler = eventHandlers[event.eventName]; + if (handler) await handler(event, stringPay); + else console.debug("SDK :: Unhandled event: ", event); + } + } catch (error) { + console.error("sdk: _handleEvent error: ", error); + } + }; + + const registerEvents = () => { + unregisterEvents(); + + window.addEventListener("message", _handleEvent); + }; + + const unregisterEvents = () => { + window.removeEventListener("message", _handleEvent); + }; + + function cleanup() { + unregisterEvents(); + + if ((window).StringPay) { + (window).StringPay.frame?.remove(); + (window).StringPay.frame = undefined; + (window).StringPay.isLoaded = false; + (window).StringPay.onFrameClose(); + } + } + // eventHandlers[Events.IFRAME_READY] = onIframeReady; eventHandlers[Events.IFRAME_CLOSE] = onIframeClose; @@ -104,8 +103,6 @@ export function createEventsService(authService: AuthService, quoteService: Quot if (!stringPay.frame || !stringPay.payload) throw new Error("Iframe not ready"); apiClient.setWalletAddress(stringPay.payload.userAddress); - apiClient.setApiKey(stringPay.payload.apiKey); - authService.setBypassDeviceCheck(stringPay.payload.options?.bypassDeviceCheck); // init fp service locationService.getFPInstance().catch((err) => console.debug("getFPInstance error: ", err)); diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index a81e215..092f468 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -4,20 +4,13 @@ import { createAuthService } from "./auth.service"; import { createQuoteService } from "./quote.service"; import { createEventsService } from "./events.service"; -export interface Services { - apiClient: ReturnType; - locationService: ReturnType; - authService: ReturnType; - quoteService: ReturnType; -} - -export function createServices({ apiUrl }: { apiUrl: string }): Services { - const apiClient = createApiClient({ baseUrl: apiUrl }); +export function createServices({ baseUrl, iframeUrl, apiKey, bypassDeviceCheck = false }: ServiceParams): Services { + const apiClient = createApiClient({ baseUrl, apiKey }); const locationService = createLocationService(); - const authService = createAuthService({ apiClient, locationService }); + const authService = createAuthService({ apiClient, locationService, bypassDeviceCheck }); const quoteService = createQuoteService(apiClient); - const eventsService = createEventsService(authService, quoteService, apiClient, locationService); + const eventsService = createEventsService(iframeUrl, authService, quoteService, apiClient, locationService); return { apiClient, @@ -28,6 +21,13 @@ export function createServices({ apiUrl }: { apiUrl: string }): Services { }; } +export interface ServiceParams { + baseUrl: string; + iframeUrl: string; + apiKey: string; + bypassDeviceCheck?: boolean; +} + // services interface export interface Services { apiClient: ReturnType; diff --git a/src/lib/services/location.service.ts b/src/lib/services/location.service.ts index 77b783e..434e9b8 100644 --- a/src/lib/services/location.service.ts +++ b/src/lib/services/location.service.ts @@ -1,6 +1,6 @@ import * as FingerprintJS from '@fingerprintjs/fingerprintjs-pro'; -const CUSTOM_SUBDOMAIN = "https://metrics.string.xyz"; +const CUSTOM_SUBDOMAIN = import.meta.env.VITE_ANALYTICS_SUBDOMAIN_URL || ""; const apiKey = import.meta.env.VITE_ANALYTICS_LIB_PK || ""; export function createLocationService(options = {}): LocationService { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e05b42b..1cd77b3 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -9,7 +9,6 @@ const signerAddress = writable(""); $: payload = { - apiKey, name: "String Demo NFT", collection: "String Demo", imageSrc: @@ -29,6 +28,16 @@ $: disabled = !$signerAddress; onMount(async () => { + if (!window.StringPay) { + console.error("[String Pay] Cannot find stringpay module in DOM"); + } + + window.StringPay.init({ + env: "LOCAL", + publicKey: apiKey + }); + + const accounts = await window.ethereum.request({ method: "eth_requestAccounts", }); diff --git a/src/test/Events.spec.ts b/src/test/Events.spec.ts index 8af54a2..d85023e 100644 --- a/src/test/Events.spec.ts +++ b/src/test/Events.spec.ts @@ -4,7 +4,7 @@ import { Events } from "$lib/services/events.service"; describe.skip("Events.ts", () => { beforeEach(() => { - (window).StringPay = new StringPay(() => {}); + (window).StringPay = new StringPay(); window.StringPay.frame = document.createElement("iframe"); window.StringPay.payload = testPayload; diff --git a/src/test/StringPayButton.spec.ts b/src/test/StringPayButton.spec.ts index 19787d6..7348382 100644 --- a/src/test/StringPayButton.spec.ts +++ b/src/test/StringPayButton.spec.ts @@ -15,7 +15,7 @@ describe("StringPayButton.svelte", () => { container.classList.add("string-pay-frame"); document.body.appendChild(container); - (window).StringPay = new StringPay(() => {}); + (window).StringPay = new StringPay(); render(StringPayButton, { payload: testPayload }); const button = screen.getByRole("button"); diff --git a/src/test/mock.ts b/src/test/mock.ts index 18c0d1a..9882beb 100644 --- a/src/test/mock.ts +++ b/src/test/mock.ts @@ -1,10 +1,8 @@ import type { StringPayload } from '../lib/StringPay'; -const apiKey = import.meta.env.VITE_STRING_API_KEY const userAddr = "0x000000" export const testPayload: StringPayload = { - apiKey, name: "String Demo NFT", collection: "String Demo", imageSrc: "https://gateway.pinata.cloud/ipfs/bafybeibtmy26mac47n5pp6srds76h74riqs76erw24p5yvdhmwu7pxlcx4/STR_Logo_1.png",