Skip to content
14 changes: 14 additions & 0 deletions components/PiPayButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<button onClick={pay} disabled={status === "pending"}>
{status === "pending" ? "Processing..." : "Pay with Pi"}
</button>
)
}
12 changes: 12 additions & 0 deletions components/components/PiAuthGuard.tsx
Original file line number Diff line number Diff line change
@@ -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 <p>Loading Pi SDK...</p>
if (!connected) return <p>Please authenticate with Pi</p>

return <>{children}</>
}
16 changes: 16 additions & 0 deletions core/core/core/payment-validator.ts
Original file line number Diff line number Diff line change
@@ -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")
}
}
9 changes: 9 additions & 0 deletions core/core/pi-loader.ts
Original file line number Diff line number Diff line change
@@ -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 <script src='https://sdk.minepi.com/pi-sdk.js' />")
}
}
16 changes: 16 additions & 0 deletions core/error.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
6 changes: 6 additions & 0 deletions mock/index.ts
Original file line number Diff line number Diff line change
@@ -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"
15 changes: 15 additions & 0 deletions mock/mockPi.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
18 changes: 18 additions & 0 deletions production-grade React SDK
Original file line number Diff line number Diff line change
@@ -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
46 changes: 46 additions & 0 deletions provider/PiProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<PiContextState>({
ready: false,
user: null
})

export function PiProvider({
children,
mock = false
}: {
children: React.ReactNode
mock?: boolean
}) {
const [ready, setReady] = useState(false)
const [user, setUser] = useState<any>(null)

useEffect(() => {
if (mock) setupMockPi()
if (isPiReady()) {
setReady(true)
window.Pi.authenticate([], (auth) => {
setUser(auth.user)
})
}
}, [mock])

return (
<PiContext.Provider value={{ ready, user }}>
{children}
</PiContext.Provider>
)
}

export function usePiContext() {
return useContext(PiContext)
}
11 changes: 11 additions & 0 deletions provider/hooks/hooks/hooks/hooks/usePiServerHandshake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function usePiServerHandshake(handlers: {
onApprove?: (paymentId: string) => Promise<void>
onComplete?: (txid: string) => Promise<void>
onFail?: (reason: string) => void
}) {
return {
approve: handlers.onApprove,
complete: handlers.onComplete,
fail: handlers.onFail
}
}
44 changes: 44 additions & 0 deletions provider/hooks/hooks/hooks/usePiPayment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useState, useCallback } from "react"
import type { PaymentData } from "pi-sdk-js"
import { validatePaymentData } from "../core/payment-validator"
import { PiSdkErrorCode } from "../core/error"

type PaymentStatus = "idle" | "pending" | "success" | "failed" | "cancelled"

export function usePiPayment(paymentData: PaymentData) {
const [status, setStatus] = useState<PaymentStatus>("idle")
const [txid, setTxid] = useState<string | null>(null)
const [error, setError] = useState<any>(null)

const pay = useCallback(async () => {
try {
validatePaymentData(paymentData)
if (!window.Pi) throw { code: PiSdkErrorCode.SDK_NOT_LOADED }

setStatus("pending")

await window.Pi.createPayment(paymentData, {
onReadyForServerApproval(paymentId) {
// kirim paymentId ke backend
},
onReadyForServerCompletion(paymentId, txid) {
setTxid(txid)
},
onCancel() {
setStatus("cancelled")
},
onError(err) {
setError(err)
setStatus("failed")
}
})

setStatus("success")
} catch (e) {
setError(e)
setStatus("failed")
}
}, [paymentData])

return { pay, status, txid, error }
}
30 changes: 30 additions & 0 deletions provider/hooks/hooks/usePiAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useState, useCallback } from "react"
import { PiSdkError, PiSdkErrorCode } from "../core/error"

export function usePiAuth() {
const [user, setUser] = useState<any>(null)
const [authenticated, setAuthenticated] = useState(false)

const authenticate = useCallback(async () => {
if (!window.Pi) {
throw new PiSdkError(PiSdkErrorCode.SDK_NOT_LOADED)
}

return new Promise((resolve, reject) => {
window.Pi.authenticate([], (auth) => {
setUser(auth.user)
setAuthenticated(true)
resolve(auth.user)
}, (err) => {
reject(err)
})
})
}, [])

const logout = () => {
setUser(null)
setAuthenticated(false)
}

return { user, authenticated, authenticate, logout }
}
10 changes: 10 additions & 0 deletions provider/hooks/usePiConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { usePiContext } from "../provider/PiProvider"

export function usePiConnection() {
const { ready, user } = usePiContext()
return {
ready,
connected: !!user,
user
}
}