diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 858b5be..6e0aa6e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: push: branches: - main - + permissions: contents: write pull-requests: write @@ -53,8 +53,15 @@ jobs: - name: Build run: pnpm build - - name: Publish to npm - run: pnpm publish --report-summary + - name: Publish Core to npm + working-directory: ./packages/core + run: pnpm publish --report-summary --no-git-checks + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish React to npm + working-directory: ./packages/react + run: pnpm publish --report-summary --no-git-checks env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -83,7 +90,14 @@ jobs: - name: Build run: pnpm build - - name: Publish to GitHub - run: pnpm publish --report-summary + - name: Publish Core to GitHub + working-directory: ./packages/core + run: pnpm publish --report-summary --no-git-checks + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish React to GitHub + working-directory: ./packages/react + run: pnpm publish --report-summary --no-git-checks env: - NODE_AUTH_TOKEN: ${{ secrets.GIT_HUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/demo/with-next/components/ClientContent/RequestPermissions.tsx b/demo/with-next/components/ClientContent/RequestPermissions.tsx new file mode 100644 index 0000000..9a677dc --- /dev/null +++ b/demo/with-next/components/ClientContent/RequestPermissions.tsx @@ -0,0 +1,145 @@ +import { + MiniKit, + RequestPermissionErrorCodes, + ResponseEvent, + Contact, + RequestPermissionPayload, + Permission, +} from "@worldcoin/minikit-js"; +import { useCallback, useEffect, useState } from "react"; +import { validateSchema } from "./helpers/validate-schema"; +import * as yup from "yup"; + +const requestPermissionSuccessPayloadSchema = yup.object({ + status: yup.string<"success">().equals(["success"]).required(), + version: yup.number().required(), + permission: yup.string().oneOf(Object.values(Permission)), + timestamp: yup.string().required(), +}); + +const requestPermissionErrorPayloadSchema = yup.object({ + error_code: yup + .string() + .oneOf(Object.values(RequestPermissionErrorCodes)) + .required(), + description: yup.string().required(), + status: yup.string<"error">().equals(["error"]).required(), + version: yup.number().required(), +}); + +export const RequestPermission = () => { + const [requestPermissionAppPayload, setRequestPermissionAppPayload] = + useState(); + + const [ + requestPermissionPayloadValidationMessage, + setRequestPermissionPayloadValidationMessage, + ] = useState(); + + const [sentRequestPermissionPayload, setSentRequestPermissionPayload] = + useState | null>(null); + + const [tempInstallFix, setTempInstallFix] = useState(0); + + useEffect(() => { + if (!MiniKit.isInstalled()) { + return; + } + + MiniKit.subscribe( + ResponseEvent.MiniAppRequestPermission, + async (payload) => { + console.log("MiniAppRequestPermission, SUBSCRIBE PAYLOAD", payload); + setRequestPermissionAppPayload(JSON.stringify(payload, null, 2)); + if (payload.status === "error") { + const errorMessage = await validateSchema( + requestPermissionErrorPayloadSchema, + payload + ); + + if (!errorMessage) { + setRequestPermissionPayloadValidationMessage("Payload is valid"); + } else { + setRequestPermissionPayloadValidationMessage(errorMessage); + } + } else { + const errorMessage = await validateSchema( + requestPermissionSuccessPayloadSchema, + payload + ); + + // This checks if the response format is correct + if (!errorMessage) { + setRequestPermissionPayloadValidationMessage("Payload is valid"); + } else { + setRequestPermissionPayloadValidationMessage(errorMessage); + } + } + } + ); + + return () => { + MiniKit.unsubscribe(ResponseEvent.MiniAppRequestPermission); + }; + }, [tempInstallFix]); + + const onRequestPermission = useCallback(async (permission: Permission) => { + const requestPermissionPayload: RequestPermissionPayload = { + permission, + }; + + const payload = MiniKit.commands.requestPermission( + requestPermissionPayload + ); + setSentRequestPermissionPayload({ + payload, + }); + console.log("payload", payload); + setTempInstallFix((prev) => prev + 1); + }, []); + + return ( +
+
+

Request Permission

+ +
+
+
+              {JSON.stringify(sentRequestPermissionPayload, null, 2)}
+            
+
+
+
+ +
+
+ +
+ +
+

+ Message from "{ResponseEvent.MiniAppRequestPermission}"{" "} +

+ +
+
+            {requestPermissionAppPayload ?? JSON.stringify(null)}
+          
+
+ +
+

Response Validation:

+

+ {requestPermissionPayloadValidationMessage ?? "No validation"} +

+
+
+
+ ); +}; diff --git a/demo/with-next/components/ClientContent/ShareContacts.tsx b/demo/with-next/components/ClientContent/ShareContacts.tsx index 4124b24..a418caa 100644 --- a/demo/with-next/components/ClientContent/ShareContacts.tsx +++ b/demo/with-next/components/ClientContent/ShareContacts.tsx @@ -79,9 +79,10 @@ export const ShareContacts = () => { }, [tempInstallFix]); const onShareContacts = useCallback( - async (isMultiSelectEnabled: boolean = false) => { + async (isMultiSelectEnabled: boolean = false, inviteMessage?: string) => { const shareContactsPayload: ShareContactsPayload = { isMultiSelectEnabled, + inviteMessage, }; const payload = MiniKit.commands.shareContacts(shareContactsPayload); @@ -120,6 +121,14 @@ export const ShareContacts = () => { Share Contacts (multi disabled) +
+ +

diff --git a/demo/with-next/components/ClientContent/Transaction.tsx b/demo/with-next/components/ClientContent/Transaction.tsx index a90b388..27a2608 100644 --- a/demo/with-next/components/ClientContent/Transaction.tsx +++ b/demo/with-next/components/ClientContent/Transaction.tsx @@ -3,6 +3,9 @@ import { MiniKit, ResponseEvent, SendTransactionErrorCodes, + Tokens, + tokenToDecimals, + VerificationLevel, } from "@worldcoin/minikit-js"; import { useWaitForTransactionReceipt } from "@worldcoin/minikit-react"; import { useEffect, useState } from "react"; @@ -141,7 +144,7 @@ export const SendTransaction = () => { const permitTransfer = { permitted: { token: testTokens.worldchain.USDCE, - amount: "10000", + amount: "200000", }, nonce: Date.now().toString(), deadline, @@ -155,7 +158,7 @@ export const SendTransaction = () => { const transferDetails = { to: "0x126f7998Eb44Dd2d097A8AB2eBcb28dEA1646AC8", - requestedAmount: "10000", + requestedAmount: "200000", }; const transferDetailsArgsForm = [ @@ -394,6 +397,36 @@ export const SendTransaction = () => { setTransactionData(payload); }; + const doubleAction = async () => { + const payload = await MiniKit.commandsAsync.verify({ + action: process.env.NEXT_PUBLIC_STAGING_VERIFY_ACTION || "", + signal: "123", + verification_level: VerificationLevel.Device, + }); + const pay = await MiniKit.commandsAsync.pay({ + to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + tokens: [ + { + symbol: Tokens.WLD, + token_amount: tokenToDecimals(0.1, Tokens.WLD).toString(), + }, + ], + description: "Test Chaining", + reference: new Date().toISOString(), + }); + // onSendTransactionClick(); + }; + + const doubleActionTransact = async () => { + const payload = await MiniKit.commandsAsync.verify({ + action: process.env.NEXT_PUBLIC_STAGING_VERIFY_ACTION || "", + signal: "123", + verification_level: VerificationLevel.Device, + }); + + onSendTransactionClick(); + }; + return (

Transaction

@@ -413,7 +446,7 @@ export const SendTransaction = () => { className="bg-black text-white rounded-lg p-4 w-full" onClick={onSendTransactionClick} > - Send Transaction + Simulation Fails
+
+ + +

diff --git a/demo/with-next/components/ClientContent/index.tsx b/demo/with-next/components/ClientContent/index.tsx index 2e210c0..65333e6 100644 --- a/demo/with-next/components/ClientContent/index.tsx +++ b/demo/with-next/components/ClientContent/index.tsx @@ -12,6 +12,7 @@ import { SendTransaction } from "./Transaction"; import { SignMessage } from "./SignMessage"; import { SignTypedData } from "./SignTypedMessage"; import { ShareContacts } from "./ShareContacts"; +import { RequestPermission } from "./RequestPermissions"; import { GetSearchedUsernameResult, UsernameSearch, @@ -98,6 +99,8 @@ export const ClientContent = () => {



+ +

diff --git a/packages/core/helpers/payment/client.ts b/packages/core/helpers/payment/client.ts index a8d30f0..9472dfa 100644 --- a/packages/core/helpers/payment/client.ts +++ b/packages/core/helpers/payment/client.ts @@ -1,5 +1,5 @@ -import { PayCommandInput, Tokens } from "types"; -import { TokenDecimals } from "types/payment"; +import { PayCommandInput } from "types/commands"; +import { TokenDecimals, Tokens } from "types/payment"; // This is a helper function to convert token amount to decimals for payment // Amount should be in expected amount ie $25.12 should be 25.12 diff --git a/packages/core/helpers/siwe/siwe.ts b/packages/core/helpers/siwe/siwe.ts index 24c84a4..3e487dc 100644 --- a/packages/core/helpers/siwe/siwe.ts +++ b/packages/core/helpers/siwe/siwe.ts @@ -7,7 +7,7 @@ import { getContract, Client, } from "viem"; -import { optimism, worldchain } from "viem/chains"; +import { worldchain } from "viem/chains"; const PREAMBLE = " wants you to sign in with your Ethereum account:"; const URI_TAG = "URI: "; @@ -189,7 +189,8 @@ export const verifySiweMessage = async ( // Check ERC-191 Signature Matches let provider = - userProvider || createPublicClient({ chain: worldchain, transport: http() }); + userProvider || + createPublicClient({ chain: worldchain, transport: http() }); const signedMessage = `${ERC_191_PREFIX}${message.length}${message}`; const messageBytes = Buffer.from(signedMessage, "utf8").toString("hex"); const hashedMessage = hashMessage(signedMessage); diff --git a/packages/core/helpers/usernames/index.ts b/packages/core/helpers/usernames/index.ts index cd81fe6..2cb9107 100644 --- a/packages/core/helpers/usernames/index.ts +++ b/packages/core/helpers/usernames/index.ts @@ -1,14 +1,14 @@ export const getUserProfile = async (address: string) => { - const res = await fetch('https://usernames.worldcoin.org/api/v1/query', { - method: 'POST', + const res = await fetch("https://usernames.worldcoin.org/api/v1/query", { + method: "POST", headers: { - 'Content-Type': 'application/json' + "Content-Type": "application/json", }, body: JSON.stringify({ - addresses: [address] - }) - }) + addresses: [address], + }), + }); const usernames = await res.json(); - return usernames[0] ?? { username: null, profilePictureUrl: null }; + return usernames?.[0] ?? { username: null, profilePictureUrl: null }; }; diff --git a/packages/core/index.ts b/packages/core/index.ts index 13f7daa..da9179e 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -1,17 +1,11 @@ export { MiniKit } from "./minikit"; -export { - VerifyCommandInput, - PayCommandInput, - Command, - SiweMessage, -} from "./types"; - export * from "./types/responses"; export * from "./types/commands"; export * from "./types/errors"; +export * from "./types/payment"; +export * from "./types/wallet-auth"; -export { Tokens, Network, TokenDecimals } from "./types/payment"; export { tokenToDecimals } from "helpers/payment/client"; export { VerificationLevel, type ISuccessResult } from "@worldcoin/idkit-core"; diff --git a/packages/core/minikit.ts b/packages/core/minikit.ts index 706a196..ced60c7 100644 --- a/packages/core/minikit.ts +++ b/packages/core/minikit.ts @@ -1,12 +1,4 @@ import { sendWebviewEvent } from "./helpers/send-webview-event"; -import { - VerifyCommandInput, - PayCommandInput, - EventPayload, - EventHandler, - WebViewBasePayload, - Command, -} from "./types"; import { MiniAppPaymentPayload, MiniAppSendTransactionPayload, @@ -16,12 +8,20 @@ import { MiniAppWalletAuthPayload, MiniAppShareContactsPayload, ResponseEvent, + MiniAppRequestPermissionPayload, + EventHandler, + EventPayload, } from "./types/responses"; import { Network } from "types/payment"; import { AsyncHandlerReturn, + Command, CommandReturnPayload, + MiniKitInstallReturnType, + PayCommandInput, PayCommandPayload, + RequestPermissionInput, + RequestPermissionPayload, SendTransactionInput, SendTransactionPayload, ShareContactsPayload, @@ -29,23 +29,23 @@ import { SignMessagePayload, SignTypedDataInput, SignTypedDataPayload, + VerifyCommandInput, VerifyCommandPayload, WalletAuthInput, WalletAuthPayload, + WebViewBasePayload, } from "types/commands"; import { VerificationLevel } from "@worldcoin/idkit-core"; import { generateSignal, encodeAction } from "@worldcoin/idkit-core/hashing"; - import { validateWalletAuthCommandInput } from "helpers/siwe/validate-wallet-auth-command-input"; import { generateSiweMessage } from "helpers/siwe/siwe"; -import { - MiniKitInstallErrorCodes, - MiniKitInstallReturnType, - MiniKitInstallErrorMessage, -} from "types"; import { validatePaymentPayload } from "helpers/payment/client"; import { getUserProfile } from "helpers/usernames"; import { User } from "./types/user"; +import { + MiniKitInstallErrorCodes, + MiniKitInstallErrorMessage, +} from "types/errors"; export const sendMiniKitEvent = < T extends WebViewBasePayload = WebViewBasePayload, @@ -66,6 +66,7 @@ export class MiniKit { [Command.SignMessage]: 1, [Command.SignTypedData]: 1, [Command.ShareContacts]: 1, + [Command.RequestPermission]: 1, }; private static isCommandAvailable = { @@ -76,6 +77,7 @@ export class MiniKit { [Command.SignMessage]: false, [Command.SignTypedData]: false, [Command.ShareContacts]: false, + [Command.RequestPermission]: false, }; private static listeners: Record = { @@ -86,6 +88,7 @@ export class MiniKit { [ResponseEvent.MiniAppSignMessage]: () => {}, [ResponseEvent.MiniAppSignTypedData]: () => {}, [ResponseEvent.MiniAppShareContacts]: () => {}, + [ResponseEvent.MiniAppRequestPermission]: () => {}, }; public static appId: string | null = null; @@ -291,6 +294,7 @@ export class MiniKit { return eventPayload; }, + pay: (payload: PayCommandInput): PayCommandPayload | null => { if ( typeof window === "undefined" || @@ -322,6 +326,7 @@ export class MiniKit { return eventPayload; }, + walletAuth: (payload: WalletAuthInput): WalletAuthPayload | null => { if ( typeof window === "undefined" || @@ -379,6 +384,7 @@ export class MiniKit { return walletAuthPayload; }, + sendTransaction: ( payload: SendTransactionInput ): SendTransactionPayload | null => { @@ -401,6 +407,7 @@ export class MiniKit { return payload; }, + signMessage: (payload: SignMessageInput): SignMessagePayload | null => { if ( typeof window === "undefined" || @@ -421,6 +428,7 @@ export class MiniKit { return payload; }, + signTypedData: ( payload: SignTypedDataInput ): SignTypedDataPayload | null => { @@ -466,6 +474,28 @@ export class MiniKit { return payload; }, + + requestPermission: ( + payload: RequestPermissionInput + ): RequestPermissionPayload | null => { + if ( + typeof window === "undefined" || + !this.isCommandAvailable[Command.RequestPermission] + ) { + console.error( + "'requestPermission' command is unavailable. Check MiniKit.install() or update the app version" + ); + return null; + } + + sendMiniKitEvent({ + command: Command.RequestPermission, + version: 1, + payload, + }); + + return payload; + }, }; /** @@ -609,5 +639,25 @@ export class MiniKit { } }); }, + + requestPermission: async ( + payload: RequestPermissionInput + ): AsyncHandlerReturn< + RequestPermissionPayload | null, + MiniAppRequestPermissionPayload + > => { + return new Promise(async (resolve, reject) => { + try { + const response = await MiniKit.awaitCommand( + ResponseEvent.MiniAppRequestPermission, + Command.RequestPermission, + () => this.commands.requestPermission(payload) + ); + resolve(response); + } catch (error) { + reject(error); + } + }); + }, }; } diff --git a/packages/core/types/commands.ts b/packages/core/types/commands.ts index 3cca984..9d5a99e 100644 --- a/packages/core/types/commands.ts +++ b/packages/core/types/commands.ts @@ -12,6 +12,7 @@ export enum Command { SignMessage = "sign-message", SignTypedData = "sign-typed-data", ShareContacts = "share-contacts", + RequestPermission = "request-permission", } export type WebViewBasePayload = { @@ -97,9 +98,21 @@ export type SignTypedDataPayload = SignTypedDataInput; // Anchor: Share Contacts Payload export type ShareContactsInput = { isMultiSelectEnabled: boolean; + inviteMessage?: string; }; export type ShareContactsPayload = ShareContactsInput; +// Anchor: Request Permission Payload +export enum Permission { + Notifications = "notifications", +} + +export type RequestPermissionInput = { + permission: Permission; +}; + +export type RequestPermissionPayload = RequestPermissionInput; + type CommandReturnPayloadMap = { [Command.Verify]: VerifyCommandPayload; [Command.Pay]: PayCommandPayload; @@ -108,6 +121,7 @@ type CommandReturnPayloadMap = { [Command.SignMessage]: SignMessagePayload; [Command.SignTypedData]: SignTypedDataPayload; [Command.ShareContacts]: ShareContactsPayload; + [Command.RequestPermission]: RequestPermissionPayload; }; export type CommandReturnPayload = T extends keyof CommandReturnPayloadMap ? CommandReturnPayloadMap[T] : never; diff --git a/packages/core/types/errors.ts b/packages/core/types/errors.ts index 63e0045..83644e2 100644 --- a/packages/core/types/errors.ts +++ b/packages/core/types/errors.ts @@ -157,3 +157,26 @@ export const ShareContactsErrorMessage = { [ShareContactsErrorCodes.UserRejected]: "User rejected the request.", [ShareContactsErrorCodes.GenericError]: "Something unexpected went wrong.", }; + +export enum RequestPermissionErrorCodes { + UserRejected = "user_rejected", + GenericError = "generic_error", + AlreadyRequested = "already_requested", + PermissionDisabled = "permission_disabled", + AlreadyGranted = "already_granted", + UnsupportedPermission = "unsupported_permission", +} + +export const RequestPermissionErrorMessage = { + [RequestPermissionErrorCodes.UserRejected]: "User declined sharing contacts", + [RequestPermissionErrorCodes.GenericError]: + "Request failed for unknown reason.", + [RequestPermissionErrorCodes.AlreadyRequested]: + "User has already declined turning on notifications once", + [RequestPermissionErrorCodes.PermissionDisabled]: + "User does not have this permission enabled in World App", + [RequestPermissionErrorCodes.AlreadyGranted]: + "If the user has already granted this mini app permission", + [RequestPermissionErrorCodes.UnsupportedPermission]: + "The permission requested is not supported by this mini app", +}; diff --git a/packages/core/types/index.ts b/packages/core/types/index.ts deleted file mode 100644 index 5dc0168..0000000 --- a/packages/core/types/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -export { - VerifyCommandInput, - PayCommandInput, - WebViewBasePayload, - Command, - MiniKitInstallReturnType, -} from "./commands"; - -export { - MiniAppVerifyActionPayload, - MiniAppPaymentPayload, - MiniAppWalletAuthPayload, - EventPayload, - EventHandler, -} from "./responses"; - -export { Tokens } from "./payment"; -export { SiweMessage } from "./wallet-auth"; -export { MiniKitInstallErrorCodes, MiniKitInstallErrorMessage } from "./errors"; diff --git a/packages/core/types/responses.ts b/packages/core/types/responses.ts index c3ae7c3..cd7ec2e 100644 --- a/packages/core/types/responses.ts +++ b/packages/core/types/responses.ts @@ -1,6 +1,7 @@ import { Network } from "./payment"; import { PaymentErrorCodes, + RequestPermissionErrorCodes, SendTransactionErrorCodes, ShareContactsErrorCodes, SignMessageErrorCodes, @@ -10,6 +11,7 @@ import { WalletAuthErrorMessage, } from "./errors"; import { VerificationLevel } from "@worldcoin/idkit-core"; +import { Permission } from "./commands"; export enum ResponseEvent { MiniAppVerifyAction = "miniapp-verify-action", @@ -19,6 +21,7 @@ export enum ResponseEvent { MiniAppSignMessage = "miniapp-sign-message", MiniAppSignTypedData = "miniapp-sign-typed-data", MiniAppShareContacts = "miniapp-share-contacts", + MiniAppRequestPermission = "miniapp-request-permission", } export type MiniAppVerifyActionSuccessPayload = { @@ -162,6 +165,25 @@ export type MiniAppShareContactsPayload = | MiniAppShareContactsSuccessPayload | MiniAppShareContactsErrorPayload; +// Anchor: Request Permission Payload +export type MiniAppRequestPermissionSuccessPayload = { + status: "success"; + permission: Permission; + timestamp: string; + version: number; +}; + +export type MiniAppRequestPermissionErrorPayload = { + status: "error"; + error_code: RequestPermissionErrorCodes; + description: string; + version: number; +}; + +export type MiniAppRequestPermissionPayload = + | MiniAppRequestPermissionSuccessPayload + | MiniAppRequestPermissionErrorPayload; + type EventPayloadMap = { [ResponseEvent.MiniAppVerifyAction]: MiniAppVerifyActionPayload; [ResponseEvent.MiniAppPayment]: MiniAppPaymentPayload; @@ -170,6 +192,7 @@ type EventPayloadMap = { [ResponseEvent.MiniAppSignMessage]: MiniAppSignMessagePayload; [ResponseEvent.MiniAppSignTypedData]: MiniAppSignTypedDataPayload; [ResponseEvent.MiniAppShareContacts]: MiniAppShareContactsPayload; + [ResponseEvent.MiniAppRequestPermission]: MiniAppRequestPermissionPayload; }; export type EventPayload = diff --git a/release-please-config.json b/release-please-config.json index 974334b..2e09d85 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,33 +1,25 @@ { - "release-type": "node", + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", "packages": { - "packages/react": { - "component": "react", - "skip-github-release": true - }, "packages/core": { - "component": "core", - "skip-github-release": true - }, - ".": { "component": "minikit-js", - "changelog-path": "CHANGELOG.md", - "bump-minor-pre-major": false, - "bump-patch-for-minor-pre-major": false, - "draft": false, - "prerelease": false + "skip-github-release": false + }, + "packages/react": { + "component": "minikit-react", + "skip-github-release": false } }, "plugins": [ { - "type": "node-workspace", - "merge": false + "merge": false, + "type": "node-workspace" }, { - "type": "linked-versions", - "groupName": "group", - "components": ["core", "react", "minikit-js"] + "components": ["minikit-react", "minikit-js"], + "groupwhatName": "minikit-packages", + "type": "linked-versions" } ], - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" + "release-type": "node" }