Skip to content

Commit

Permalink
feat: share contacts command (#104)
Browse files Browse the repository at this point in the history
* share contacts command (#103)

* dev

* usernames example

* remove username file

* links test

* update url

* add path param

* Add Send Haptic Feedback command (#82)

* Add Send Haptic Feedback command

* add haptics to demo, improve dx

* fix: add haptic feedback async handler, simplify payloads

* Revert "Add Send Haptic Feedback command (#82)" (#114)

This reverts commit 072798a.

---------

Co-authored-by: Michał Struck <[email protected]>
  • Loading branch information
andy-t-wang and michalstruck authored Nov 26, 2024
1 parent be66b05 commit bae6d76
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 2 deletions.
8 changes: 7 additions & 1 deletion demo/with-next/components/ClientContent/ExternalLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ export const ExternalLinks = () => {
>
Valid Associated Domain (Link)
</Link>
<Link
href="worldapp://mini-app?app_id=app_staging_e387587d26a286fb5bea1d436ba0b2a3&path=features"
className="bg-green-500 text-white text-center rounded-lg p-3"
>
worldapp:// deep link
</Link>
<button
onClick={() => {
window.open(
"https://worldcoin.org/mini-app?app_id=app_staging_d3b49eb04b497130e18533b9d8846319",
"https://world.org/mini-app?app_id=app_staging_e387587d26a286fb5bea1d436ba0b2a3&path=features",
"_blank"
);
}}
Expand Down
145 changes: 145 additions & 0 deletions demo/with-next/components/ClientContent/ShareContacts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {
MiniKit,
ShareContactsErrorCodes,
ResponseEvent,
Contact,
ShareContactsPayload,
} from "@worldcoin/minikit-js";
import { useCallback, useEffect, useState } from "react";
import { validateSchema } from "./helpers/validate-schema";
import * as yup from "yup";

const shareContactsSuccessPayloadSchema = yup.object({
status: yup.string<"success">().equals(["success"]).required(),
version: yup.number().required(),
contacts: yup.array().of(yup.object<Contact>().required()),
});

const shareContactsErrorPayloadSchema = yup.object({
error_code: yup
.string<ShareContactsErrorCodes>()
.oneOf(Object.values(ShareContactsErrorCodes))
.required(),
status: yup.string<"error">().equals(["error"]).required(),
version: yup.number().required(),
});

export const ShareContacts = () => {
const [shareContactsAppPayload, setShareContactsAppPayload] = useState<
string | undefined
>();

const [
shareContactsPayloadValidationMessage,
setShareContactsPayloadValidationMessage,
] = useState<string | null>();

const [sentShareContactsPayload, setSentShareContactsPayload] =
useState<Record<string, any> | null>(null);

const [tempInstallFix, setTempInstallFix] = useState(0);

useEffect(() => {
if (!MiniKit.isInstalled()) {
return;
}

MiniKit.subscribe(ResponseEvent.MiniAppShareContacts, async (payload) => {
console.log("MiniAppShareContacts, SUBSCRIBE PAYLOAD", payload);
setShareContactsAppPayload(JSON.stringify(payload, null, 2));
if (payload.status === "error") {
const errorMessage = await validateSchema(
shareContactsErrorPayloadSchema,
payload
);

if (!errorMessage) {
setShareContactsPayloadValidationMessage("Payload is valid");
} else {
setShareContactsPayloadValidationMessage(errorMessage);
}
} else {
const errorMessage = await validateSchema(
shareContactsSuccessPayloadSchema,
payload
);

// This checks if the response format is correct
if (!errorMessage) {
setShareContactsPayloadValidationMessage("Payload is valid");
} else {
setShareContactsPayloadValidationMessage(errorMessage);
}
}
});

return () => {
MiniKit.unsubscribe(ResponseEvent.MiniAppShareContacts);
};
}, [tempInstallFix]);

const onShareContacts = useCallback(
async (isMultiSelectEnabled: boolean = false) => {
const shareContactsPayload: ShareContactsPayload = {
isMultiSelectEnabled,
};

const payload = MiniKit.commands.shareContacts(shareContactsPayload);
setSentShareContactsPayload({
payload,
});
console.log("payload", payload);
setTempInstallFix((prev) => prev + 1);
},
[]
);

return (
<div>
<div className="grid gap-y-2">
<h2 className="text-2xl font-bold">Share Contacts</h2>

<div>
<div className="bg-gray-300 min-h-[100px] p-2">
<pre className="break-all whitespace-break-spaces">
{JSON.stringify(sentShareContactsPayload, null, 2)}
</pre>
</div>
</div>
<div className="grid gap-4 grid-cols-2">
<button
className="bg-black text-white rounded-lg p-4 w-full"
onClick={() => onShareContacts(true)}
>
Share Contacts (Multi enabled)
</button>
<button
className="bg-black text-white rounded-lg p-4 w-full"
onClick={() => onShareContacts(false)}
>
Share Contacts (multi disabled)
</button>
</div>
</div>

<hr />

<div className="w-full grid gap-y-2">
<p>Message from &quot;{ResponseEvent.MiniAppShareContacts}&quot; </p>

<div className="bg-gray-300 min-h-[100px] p-2">
<pre className="break-all whitespace-break-spaces">
{shareContactsAppPayload ?? JSON.stringify(null)}
</pre>
</div>

<div className="grid gap-y-2">
<p>Response Validation:</p>
<p className="bg-gray-300 p-2">
{shareContactsPayloadValidationMessage ?? "No validation"}
</p>
</div>
</div>
</div>
);
};
56 changes: 56 additions & 0 deletions demo/with-next/components/ClientContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,28 @@ import { CameraComponent } from "./Camera";
import { SendTransaction } from "./Transaction";
import { SignMessage } from "./SignMessage";
import { SignTypedData } from "./SignTypedMessage";
import { ShareContacts } from "./ShareContacts";
import {
GetSearchedUsernameResult,
UsernameSearch,
} from "@worldcoin/minikit-react";
import { useState } from "react";
import Image from "next/image";

const VersionsNoSSR = dynamic(
() => import("./Versions").then((comp) => comp.Versions),
{ ssr: false }
);

export const ClientContent = () => {
const [searchValue, setSearchValue] = useState("");
const [searchResults, setSearchResults] =
useState<GetSearchedUsernameResult>();

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchValue(e.target.value);
};

return (
<div className="p-2 lg:p-8 grid content-start min-h-[100dvh] gap-y-2">
<Nav />
Expand All @@ -27,6 +42,45 @@ export const ClientContent = () => {
<User />
<hr />

<UsernameSearch
value={searchValue}
handleChange={handleChange}
setSearchedUsernames={setSearchResults}
className="p-2 border rounded"
inputProps={{
placeholder: "Search usernames...",
}}
/>

{/* Display search results */}
{searchResults && (
<div className="mt-4">
{searchResults.status === 200 ? (
<ul>
{searchResults.data?.map((user) => (
<li
key={user.address}
className="grid grid-cols-[auto_1fr] gap-x-2 items-center"
>
{user.profile_picture_url && (
<Image
src={user.profile_picture_url}
alt={user.username}
width={32}
height={32}
className="w-10 h-10 rounded-full"
/>
)}
{user.username} - {user.address}
</li>
))}
</ul>
) : (
<p>Error: {searchResults.error}</p>
)}
</div>
)}

<div className="grid gap-y-8">
<VersionsNoSSR />
<hr />
Expand All @@ -42,6 +96,8 @@ export const ClientContent = () => {
<hr />
<SignTypedData />
<hr />
<ShareContacts />
<hr />
<input className="text-xs border-black border-2" />
<ExternalLinks />
<hr />
Expand Down
6 changes: 5 additions & 1 deletion demo/with-next/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
images: {
domains: ["static.usernames.app-backend.toolsforhumanity.com"],
},
};

export default nextConfig;
47 changes: 47 additions & 0 deletions packages/core/minikit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
MiniAppSignTypedDataPayload,
MiniAppVerifyActionPayload,
MiniAppWalletAuthPayload,
MiniAppShareContactsPayload,
ResponseEvent,
} from "./types/responses";
import { Network } from "types/payment";
Expand All @@ -23,6 +24,7 @@ import {
PayCommandPayload,
SendTransactionInput,
SendTransactionPayload,
ShareContactsPayload,
SignMessageInput,
SignMessagePayload,
SignTypedDataInput,
Expand Down Expand Up @@ -62,6 +64,7 @@ export class MiniKit {
[Command.SendTransaction]: 1,
[Command.SignMessage]: 1,
[Command.SignTypedData]: 1,
[Command.ShareContacts]: 1,
};

private static isCommandAvailable = {
Expand All @@ -71,6 +74,7 @@ export class MiniKit {
[Command.SendTransaction]: false,
[Command.SignMessage]: false,
[Command.SignTypedData]: false,
[Command.ShareContacts]: false,
};

private static listeners: Record<ResponseEvent, EventHandler> = {
Expand All @@ -80,6 +84,7 @@ export class MiniKit {
[ResponseEvent.MiniAppSendTransaction]: () => {},
[ResponseEvent.MiniAppSignMessage]: () => {},
[ResponseEvent.MiniAppSignTypedData]: () => {},
[ResponseEvent.MiniAppShareContacts]: () => {},
};

public static appId: string | null = null;
Expand Down Expand Up @@ -432,6 +437,29 @@ export class MiniKit {

return payload;
},

shareContacts: (
payload: ShareContactsPayload
): ShareContactsPayload | null => {
if (
typeof window === "undefined" ||
!this.isCommandAvailable[Command.SignTypedData]
) {
console.error(
"'shareContacts' command is unavailable. Check MiniKit.install() or update the app version"
);

return null;
}

sendMiniKitEvent<WebViewBasePayload>({
command: Command.ShareContacts,
version: 1,
payload,
});

return payload;
},
};

/**
Expand Down Expand Up @@ -556,5 +584,24 @@ export class MiniKit {
}
});
},
shareContacts: async (
payload: ShareContactsPayload
): AsyncHandlerReturn<
ShareContactsPayload | null,
MiniAppShareContactsPayload
> => {
return new Promise(async (resolve, reject) => {
try {
const response = await MiniKit.awaitCommand(
ResponseEvent.MiniAppShareContacts,
Command.ShareContacts,
() => this.commands.shareContacts(payload)
);
return resolve(response);
} catch (error) {
reject(error);
}
});
},
};
}
8 changes: 8 additions & 0 deletions packages/core/types/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum Command {
SendTransaction = "send-transaction",
SignMessage = "sign-message",
SignTypedData = "sign-typed-data",
ShareContacts = "share-contacts",
}

export type WebViewBasePayload = {
Expand Down Expand Up @@ -93,13 +94,20 @@ export type SignTypedDataInput = {

export type SignTypedDataPayload = SignTypedDataInput;

// Anchor: Share Contacts Payload
export type ShareContactsInput = {
isMultiSelectEnabled: boolean;
};
export type ShareContactsPayload = ShareContactsInput;

type CommandReturnPayloadMap = {
[Command.Verify]: VerifyCommandPayload;
[Command.Pay]: PayCommandPayload;
[Command.WalletAuth]: WalletAuthPayload;
[Command.SendTransaction]: SendTransactionPayload;
[Command.SignMessage]: SignMessagePayload;
[Command.SignTypedData]: SignTypedDataPayload;
[Command.ShareContacts]: ShareContactsPayload;
};
export type CommandReturnPayload<T extends Command> =
T extends keyof CommandReturnPayloadMap ? CommandReturnPayloadMap[T] : never;
10 changes: 10 additions & 0 deletions packages/core/types/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,13 @@ export const MiniKitInstallErrorMessage = {
[MiniKitInstallErrorCodes.AppOutOfDate]:
"WorldApp is out of date. Please update the app.",
};

export enum ShareContactsErrorCodes {
UserRejected = "user_rejected",
GenericError = "generic_error",
}

export const ShareContactsErrorMessage = {
[ShareContactsErrorCodes.UserRejected]: "User rejected the request.",
[ShareContactsErrorCodes.GenericError]: "Something unexpected went wrong.",
};
Loading

0 comments on commit bae6d76

Please sign in to comment.