diff --git a/components/PiPayButton.tsx b/components/PiPayButton.tsx new file mode 100644 index 0000000..f49a662 --- /dev/null +++ b/components/PiPayButton.tsx @@ -0,0 +1,14 @@ +"use client" + +import { usePiPayment } from "../hooks/usePiPayment" +import type { PaymentData } from "pi-sdk-js" + +export function PiPayButton({ paymentData }: { paymentData: PaymentData }) { + const { pay, status } = usePiPayment(paymentData) + + return ( + + ) +} diff --git a/components/components/PiAuthGuard.tsx b/components/components/PiAuthGuard.tsx new file mode 100644 index 0000000..0ee0dc0 --- /dev/null +++ b/components/components/PiAuthGuard.tsx @@ -0,0 +1,12 @@ +"use client" + +import { usePiConnection } from "../hooks/usePiConnection" + +export function PiAuthGuard({ children }: { children: React.ReactNode }) { + const { connected, ready } = usePiConnection() + + if (!ready) return
Loading Pi SDK...
+ if (!connected) returnPlease authenticate with Pi
+ + return <>{children}> +} diff --git a/core/core/core/payment-validator.ts b/core/core/core/payment-validator.ts new file mode 100644 index 0000000..304cf90 --- /dev/null +++ b/core/core/core/payment-validator.ts @@ -0,0 +1,16 @@ +import type { PaymentData } from "pi-sdk-js" +import { PiSdkError, PiSdkErrorCode } from "./error" + +export function validatePaymentData(data: PaymentData) { + if (!data.amount || data.amount <= 0) { + throw new PiSdkError(PiSdkErrorCode.INVALID_PAYMENT, "Invalid amount") + } + if (!data.memo) { + throw new PiSdkError(PiSdkErrorCode.INVALID_PAYMENT, "Memo is required") + } + try { + JSON.stringify(data.metadata ?? {}) + } catch { + throw new PiSdkError(PiSdkErrorCode.INVALID_PAYMENT, "Metadata must be serializable") + } +} diff --git a/core/core/pi-loader.ts b/core/core/pi-loader.ts new file mode 100644 index 0000000..597f7e8 --- /dev/null +++ b/core/core/pi-loader.ts @@ -0,0 +1,9 @@ +export function isPiReady(): boolean { + return typeof window !== "undefined" && !!window.Pi +} + +export function assertPiReady() { + if (!isPiReady()) { + throw new Error("Pi SDK not loaded. Ensure ") + } +} diff --git a/core/error.ts b/core/error.ts new file mode 100644 index 0000000..f3d6703 --- /dev/null +++ b/core/error.ts @@ -0,0 +1,16 @@ +export enum PiSdkErrorCode { + SDK_NOT_LOADED = "SDK_NOT_LOADED", + NOT_READY = "NOT_READY", + USER_REJECTED = "USER_REJECTED", + INVALID_PAYMENT = "INVALID_PAYMENT", + NETWORK_ERROR = "NETWORK_ERROR", + UNKNOWN = "UNKNOWN" +} + +export class PiSdkError extends Error { + code: PiSdkErrorCode + constructor(code: PiSdkErrorCode, message?: string) { + super(message ?? code) + this.code = code + } +} diff --git a/mock/index.ts b/mock/index.ts new file mode 100644 index 0000000..8ecc4cb --- /dev/null +++ b/mock/index.ts @@ -0,0 +1,6 @@ +export * from "./provider/PiProvider" +export * from "./hooks/usePiConnection" +export * from "./hooks/usePiAuth" +export * from "./hooks/usePiPayment" +export * from "./components/PiPayButton" +export * from "./components/PiAuthGuard" diff --git a/mock/mockPi.ts b/mock/mockPi.ts new file mode 100644 index 0000000..76edb62 --- /dev/null +++ b/mock/mockPi.ts @@ -0,0 +1,15 @@ +export function setupMockPi() { + if (window.Pi) return + + window.Pi = { + authenticate(scopes, onSuccess) { + onSuccess({ user: { uid: "mock-user", username: "dev" } }) + }, + createPayment(data, callbacks) { + setTimeout(() => { + callbacks.onReadyForServerApproval("mock-payment-id") + callbacks.onReadyForServerCompletion("mock-payment-id", "mock-txid") + }, 1000) + } + } +} diff --git a/production-grade React SDK b/production-grade React SDK new file mode 100644 index 0000000..135a202 --- /dev/null +++ b/production-grade React SDK @@ -0,0 +1,18 @@ +src/ +├── core/ +│ ├── pi-loader.ts +│ ├── error.ts +│ └── payment-validator.ts +├── provider/ +│ └── PiProvider.tsx +├── hooks/ +│ ├── usePiConnection.ts +│ ├── usePiAuth.ts +│ ├── usePiPayment.ts +│ └── usePiServerHandshake.ts +├── components/ +│ ├── PiPayButton.tsx +│ └── PiAuthGuard.tsx +├── mock/ +│ └── mockPi.ts +└── index.ts diff --git a/provider/PiProvider.tsx b/provider/PiProvider.tsx new file mode 100644 index 0000000..141c549 --- /dev/null +++ b/provider/PiProvider.tsx @@ -0,0 +1,46 @@ +"use client" + +import React, { createContext, useContext, useEffect, useState } from "react" +import { isPiReady } from "../core/pi-loader" +import { setupMockPi } from "../mock/mockPi" + +type PiContextState = { + ready: boolean + user: any | null +} + +const PiContext = createContext