Skip to content

Commit

Permalink
feat: farcaster login
Browse files Browse the repository at this point in the history
  • Loading branch information
gregfromstl committed Jul 26, 2024
1 parent 3848327 commit 4fab8e8
Show file tree
Hide file tree
Showing 14 changed files with 47 additions and 6 deletions.
12 changes: 12 additions & 0 deletions .changeset/spicy-flowers-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"thirdweb": minor
---

Adds SIWF for in-app wallets

```ts
await wallet.connect({
strategy: "farcaster",
client: CLIENT,
});
```
2 changes: 1 addition & 1 deletion apps/playground-web/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ export const WALLETS = [
auth: {
options: [
"google",
"facebook",
"discord",
"apple",
"email",
"passkey",
"phone",
"farcaster"
],
mode: "redirect",
},
Expand Down
3 changes: 3 additions & 0 deletions packages/thirdweb/src/react/core/utils/socialIcons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const twitchIconUri =
"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgdmlld0JveD0iMCAwIDExMS43ODY2NyAxMjcuMzg2NjciCiAgIGhlaWdodD0iMTI3LjM4NjY3IgogICB3aWR0aD0iMTExLjc4NjY3IgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICB2ZXJzaW9uPSIxLjEiCiAgIGlkPSJzdmczMzU1IgogICBzb2RpcG9kaTpkb2NuYW1lPSJUd2l0Y2hfbG9nby5zdmciCiAgIGlua3NjYXBlOnZlcnNpb249IjEuMS4xICgzYmY1YWUwZDI1LCAyMDIxLTA5LTIwKSIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcwogICBpZD0iZGVmczI5Ij4KICAgIAogICAgCiAgPC9kZWZzPjxzb2RpcG9kaTpuYW1lZHZpZXcKICAgaWQ9Im5hbWVkdmlldzI3IgogICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiCiAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiCiAgIHNob3dncmlkPSJmYWxzZSIKICAgaW5rc2NhcGU6em9vbT0iNC4xOTkyMjg0IgogICBpbmtzY2FwZTpjeD0iLTUwLjYwNDUzNSIKICAgaW5rc2NhcGU6Y3k9IjE0MC4zODI5MyIKICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIyNTYwIgogICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMzg3IgogICBpbmtzY2FwZTp3aW5kb3cteD0iMTkxMiIKICAgaW5rc2NhcGU6d2luZG93LXk9Ii04IgogICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIgogICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmczMzU1IiAvPgogIDxnCiAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywxMDEuMzkzMzMsNjcuNTg5MzMyKSIKICAgaWQ9ImczMzY1Ij4KICAgICAgPHBhdGgKICAgaWQ9InBhdGgzMzY3IgogICBzdHlsZT0iZmlsbDojNjQ0MWE1O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTpub25lIgogICBkPSJtIDAsMCAtMTMuNjUyLC0xMy42NTEgaCAtMjEuNDQ1IGwgLTExLjY5OSwtMTEuNjk3IHYgMTEuNjk3IEggLTY0LjM0NCBWIDQyLjg5MyBIIDAgWiBtIC03Mi4xNDYsNTAuNjkyIC0zLjg5OSwtMTUuNTk5IHYgLTcwLjE5IGggMTcuNTUgdiAtOS43NTEgaCA5Ljc0NiBsIDkuNzUyLDkuNzUxIGggMTUuNTk2IEwgNy43OTUsLTMuOTA1IHYgNTQuNTk3IHoiIC8+CiAgICA8L2c+PHBhdGgKICAgaWQ9InBhdGgzMzY5IgogICBzdHlsZT0iZmlsbDojNjQ0MWE1O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxLjMzMzMzIgogICBkPSJtIDQ0LjE5NzMzMSw2Mi4zOTQyNjYgaCAxMC4zOTg2NyBWIDMxLjE5MjkzMyBoIC0xMC4zOTg2NyB6IG0gMjguNTk0NjcsMCBoIDEwLjM5ODY2IFYgMzEuMTkyOTMzIGggLTEwLjM5ODY2IHoiIC8+Cjwvc3ZnPgo=";
export const discordIconUri =
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEwMCAxMDAiIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBpZD0iZGlzY29yZCI+PHBhdGggZmlsbD0iIzY2NjVkMiIgZD0iTTg1LjIyLDI0Ljk1OGMtMTEuNDU5LTguNTc1LTIyLjQzOC04LjMzNC0yMi40MzgtOC4zMzRsLTEuMTIyLDEuMjgyCgkJCQljMTMuNjIzLDQuMDg3LDE5Ljk1NCwxMC4wOTcsMTkuOTU0LDEwLjA5N2MtMTkuNDkxLTEwLjczMS00NC4zMTctMTAuNjU0LTY0LjU5LDBjMCwwLDYuNTcxLTYuMzMxLDIwLjk5Ni0xMC40MThsLTAuODAxLTAuOTYyCgkJCQljMCwwLTEwLjg5OS0wLjI0LTIyLjQzOCw4LjMzNGMwLDAtMTEuNTQsMjAuNzU1LTExLjU0LDQ2LjMxOWMwLDAsNi43MzIsMTEuNTQsMjQuNDQyLDEyLjEwMWMwLDAsMi45NjUtMy41MjYsNS4zNjktNi41NzEKCQkJCWMtMTAuMTc3LTMuMDQ1LTE0LjAyNC05LjM3Ni0xNC4wMjQtOS4zNzZjNi4zOTQsNC4wMDEsMTIuODU5LDYuNTA1LDIwLjkxNiw4LjA5NGMxMy4xMDgsMi42OTgsMjkuNDEzLTAuMDc2LDQxLjU5MS04LjA5NAoJCQkJYzAsMC00LjAwNyw2LjQ5MS0xNC41MDUsOS40NTZjMi40MDQsMi45NjUsNS4yODksNi40MTEsNS4yODksNi40MTFjMTcuNzEtMC41NjEsMjQuNDQxLTEyLjEwMSwyNC40NDEtMTIuMDIKCQkJCUM5Ni43NTksNDUuNzEzLDg1LjIyLDI0Ljk1OCw4NS4yMiwyNC45NTh6IE0zNS4wNTUsNjMuODI0Yy00LjQ4OCwwLTguMTc0LTMuOTI3LTguMTc0LTguODE1CgkJCQljMC4zMjgtMTEuNzA3LDE2LjEwMi0xMS42NzEsMTYuMzQ4LDBDNDMuMjI5LDU5Ljg5NywzOS42MjIsNjMuODI0LDM1LjA1NSw2My44MjR6IE02NC4zMDQsNjMuODI0CgkJCQljLTQuNDg4LDAtOC4xNzQtMy45MjctOC4xNzQtOC44MTVjMC4zNi0xMS42ODQsMTUuOTM3LTExLjY4OSwxNi4zNDgsMEM3Mi40NzgsNTkuODk3LDY4Ljg3Miw2My44MjQsNjQuMzA0LDYzLjgyNHoiPjwvcGF0aD48L3N2Zz4=";
export const farcasterIconUri =
"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwMCIgaGVpZ2h0PSIxMDAwIiB2aWV3Qm94PSIwIDAgMTAwMCAxMDAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cmVjdCB3aWR0aD0iMTAwMCIgaGVpZ2h0PSIxMDAwIiByeD0iMjAwIiBmaWxsPSIjODU1RENEIi8+CjxwYXRoIGQ9Ik0yNTcuNzc4IDE1NS41NTZINzQyLjIyMlY4NDQuNDQ0SDY3MS4xMTFWNTI4Ljg4OUg2NzAuNDE0QzY2Mi41NTQgNDQxLjY3NyA1ODkuMjU4IDM3My4zMzMgNTAwIDM3My4zMzNDNDEwLjc0MiAzNzMuMzMzIDMzNy40NDYgNDQxLjY3NyAzMjkuNTg2IDUyOC44ODlIMzI4Ljg4OVY4NDQuNDQ0SDI1Ny43NzhWMTU1LjU1NloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xMjguODg5IDI1My4zMzNMMTU3Ljc3OCAzNTEuMTExSDE4Mi4yMjJWNzQ2LjY2N0MxNjkuOTQ5IDc0Ni42NjcgMTYwIDc1Ni42MTYgMTYwIDc2OC44ODlWNzk1LjU1NkgxNTUuNTU2QzE0My4yODMgNzk1LjU1NiAxMzMuMzMzIDgwNS41MDUgMTMzLjMzMyA4MTcuNzc4Vjg0NC40NDRIMzgyLjIyMlY4MTcuNzc4QzM4Mi4yMjIgODA1LjUwNSAzNzIuMjczIDc5NS41NTYgMzYwIDc5NS41NTZIMzU1LjU1NlY3NjguODg5QzM1NS41NTYgNzU2LjYxNiAzNDUuNjA2IDc0Ni42NjcgMzMzLjMzMyA3NDYuNjY3SDMwNi42NjdWMjUzLjMzM0gxMjguODg5WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTY3NS41NTYgNzQ2LjY2N0M2NjMuMjgzIDc0Ni42NjcgNjUzLjMzMyA3NTYuNjE2IDY1My4zMzMgNzY4Ljg4OVY3OTUuNTU2SDY0OC44ODlDNjM2LjYxNiA3OTUuNTU2IDYyNi42NjcgODA1LjUwNSA2MjYuNjY3IDgxNy43NzhWODQ0LjQ0NEg4NzUuNTU2VjgxNy43NzhDODc1LjU1NiA4MDUuNTA1IDg2NS42MDYgNzk1LjU1NiA4NTMuMzMzIDc5NS41NTZIODQ4Ljg4OVY3NjguODg5Qzg0OC44ODkgNzU2LjYxNiA4MzguOTQgNzQ2LjY2NyA4MjYuNjY3IDc0Ni42NjdWMzUxLjExMUg4NTEuMTExTDg4MCAyNTMuMzMzSDcwMi4yMjJWNzQ2LjY2N0g2NzUuNTU2WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==";
export const microsoftIconUri =
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiNmZjU3MjIiIGQ9Ik02IDZIMjJWMjJINnoiIHRyYW5zZm9ybT0icm90YXRlKC0xODAgMTQgMTQpIi8+PHBhdGggZmlsbD0iIzRjYWY1MCIgZD0iTTI2IDZINDJWMjJIMjZ6IiB0cmFuc2Zvcm09InJvdGF0ZSgtMTgwIDM0IDE0KSIvPjxwYXRoIGZpbGw9IiNmZmMxMDciIGQ9Ik0yNiAyNkg0MlY0MkgyNnoiIHRyYW5zZm9ybT0icm90YXRlKC0xODAgMzQgMzQpIi8+PHBhdGggZmlsbD0iIzAzYTlmNCIgZD0iTTYgMjZIMjJWNDJINnoiIHRyYW5zZm9ybT0icm90YXRlKC0xODAgMTQgMzQpIi8+PC9zdmc+";

Expand Down Expand Up @@ -48,4 +50,5 @@ export const socialIcons = {
apple: appleIconUri,
facebook: facebookIconUri,
discord: discordIconUri,
farcaster: farcasterIconUri,
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DISCORD_ICON,
EMAIL_ICON,
FACEBOOK_ICON,
FARCASTER_ICON,
GOOGLE_ICON,
PHONE_ICON,
WALLET_ICON,
Expand Down Expand Up @@ -76,6 +77,8 @@ export function getAuthProviderImage(lastAuthProvider: string | null): string {
return FACEBOOK_ICON;
case "discord":
return DISCORD_ICON;
case "farcaster":
return FARCASTER_ICON;
default:
return WALLET_ICON;
}
Expand Down
11 changes: 8 additions & 3 deletions packages/thirdweb/src/react/native/ui/connect/InAppWalletUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import type {
} from "../../../../wallets/in-app/core/wallet/types.js";
import { preAuthenticate } from "../../../../wallets/in-app/native/auth/index.js";
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
import {
type SocialAuthOption,
socialAuthOptions,
} from "../../../../wallets/types.js";
import type { Theme } from "../../../core/design-system/index.js";
import { setLastAuthProvider } from "../../../core/utils/storage.js";
import { radius, spacing } from "../../design-system/index.js";
Expand All @@ -26,6 +30,7 @@ import {
DISCORD_ICON,
EMAIL_ICON,
FACEBOOK_ICON,
FARCASTER_ICON,
GOOGLE_ICON,
PHONE_ICON,
} from "../icons/svgs.js";
Expand All @@ -44,6 +49,7 @@ const socialIcons = {
facebook: FACEBOOK_ICON,
apple: APPLE_ICON,
discord: DISCORD_ICON,
farcaster: FARCASTER_ICON,
};

type InAppWalletFormUIProps = {
Expand All @@ -62,9 +68,8 @@ export function InAppWalletUI(props: InAppWalletFormUIProps) {
const { wallet, theme } = props;
const config = wallet.getConfig();
const authOptions = config?.auth?.options || defaultAuthOptions;
const socialLogins = authOptions.filter(
(x) =>
x === "google" || x === "apple" || x === "facebook" || x === "discord",
const socialLogins = authOptions.filter((x) =>
socialAuthOptions.includes(x as SocialAuthOption),
) as InAppWalletSocialAuth[];

const [inputMode, setInputMode] = useState<"email" | "phone">("email");
Expand Down
7 changes: 7 additions & 0 deletions packages/thirdweb/src/react/native/ui/icons/svgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ export const DISCORD_ICON = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink
</g>
</svg>`;

export const FARCASTER_ICON = `<svg width={width} height={height} viewBox="0 0 1000 1000" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1000" height="1000" rx="200" fill="#855DCD"/>
<path d="M257.778 155.556H742.222V844.444H671.111V528.889H670.414C662.554 441.677 589.258 373.333 500 373.333C410.742 373.333 337.446 441.677 329.586 528.889H328.889V844.444H257.778V155.556Z" fill="white"/>
<path d="M128.889 253.333L157.778 351.111H182.222V746.667C169.949 746.667 160 756.616 160 768.889V795.556H155.556C143.283 795.556 133.333 805.505 133.333 817.778V844.444H382.222V817.778C382.222 805.505 372.273 795.556 360 795.556H355.556V768.889C355.556 756.616 345.606 746.667 333.333 746.667H306.667V253.333H128.889Z" fill="white"/>
<path d="M675.556 746.667C663.283 746.667 653.333 756.616 653.333 768.889V795.556H648.889C636.616 795.556 626.667 805.505 626.667 817.778V844.444H875.556V817.778C875.556 805.505 865.606 795.556 853.333 795.556H848.889V768.889C848.889 756.616 838.94 746.667 826.667 746.667V351.111H851.111L880 253.333H702.222V746.667H675.556Z" fill="white"/>
</svg>`;

export const EMAIL_ICON = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3335 2.6665H2.66683C1.93045 2.6665 1.3335 3.26346 1.3335 3.99984V11.9998C1.3335 12.7362 1.93045 13.3332 2.66683 13.3332H13.3335C14.0699 13.3332 14.6668 12.7362 14.6668 11.9998V3.99984C14.6668 3.26346 14.0699 2.6665 13.3335 2.6665Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.6668 4.6665L8.68683 8.4665C8.48101 8.59545 8.24304 8.66384 8.00016 8.66384C7.75728 8.66384 7.51931 8.59545 7.3135 8.4665L1.3335 4.6665" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
Expand Down
4 changes: 4 additions & 0 deletions packages/thirdweb/src/react/web/ui/components/WalletImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
discordIconUri,
emailIcon,
facebookIconUri,
farcasterIconUri,
genericWalletIcon,
googleIconUri,
passkeyIcon,
Expand Down Expand Up @@ -77,6 +78,9 @@ export function WalletImage(props: {
case "discord":
image = discordIconUri;
break;
case "farcaster":
image = farcasterIconUri;
break;

Check warning on line 83 in packages/thirdweb/src/react/web/ui/components/WalletImage.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/components/WalletImage.tsx#L81-L83

Added lines #L81 - L83 were not covered by tests
}
} else {
const mipdImage = getInstalledWalletProviders().find(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const ConnectWalletSocialOptions = (
facebook: locale.signInWithFacebook,
apple: locale.signInWithApple,
discord: locale.signInWithDiscord,
farcaster: "Farcaster",

Check warning on line 94 in packages/thirdweb/src/react/web/wallets/shared/ConnectWalletSocialOptions.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/wallets/shared/ConnectWalletSocialOptions.tsx#L94

Added line #L94 was not covered by tests
};

const { data: ecosystemAuthOptions, isLoading } = useQuery({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function getOauthLoginPath(
case "apple":
case "facebook":
case "google":
case "farcaster":

Check warning on line 36 in packages/thirdweb/src/react/web/wallets/shared/oauthSignIn.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/wallets/shared/oauthSignIn.ts#L36

Added line #L36 was not covered by tests
case "discord":
return getSocialAuthLoginPath(authOption, client, ecosystem);
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export enum AuthProvider {
APPLE = "Apple",
PASSKEY = "Passkey",
DISCORD = "Discord",
FARCASTER = "Farcaster",
}

export type OauthOption = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export class InAppNativeConnector implements InAppConnector {
case "google":
case "facebook":
case "discord":
case "farcaster":
case "apple": {
const ExpoLinking = require("expo-linking");
const redirectUrl =
Expand Down
5 changes: 3 additions & 2 deletions packages/thirdweb/src/wallets/in-app/web/lib/auth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,13 @@ export const loginWithOauth = async (options: {
let win = options.openedWindow;
let isWindowOpenedByFn = false;
if (!win) {
const redirectUrl = new URL(window.location.href);

Check warning on line 65 in packages/thirdweb/src/wallets/in-app/web/lib/auth/oauth.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/web/lib/auth/oauth.ts#L65

Added line #L65 was not covered by tests
win = window.open(
getSocialAuthLoginPath(
`${getSocialAuthLoginPath(

Check warning on line 67 in packages/thirdweb/src/wallets/in-app/web/lib/auth/oauth.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/web/lib/auth/oauth.ts#L67

Added line #L67 was not covered by tests
options.authOption,
options.client,
options.ecosystem,
),
)}&redirectUrl=${encodeURIComponent(redirectUrl.toString())}`,

Check warning on line 71 in packages/thirdweb/src/wallets/in-app/web/lib/auth/oauth.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/web/lib/auth/oauth.ts#L71

Added line #L71 was not covered by tests
`Login to ${options.authOption}`,
DEFAULT_POP_UP_SIZE,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export class InAppWebConnector implements InAppConnector {
case "apple":
case "facebook":
case "google":
case "farcaster":

Check warning on line 225 in packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts#L225

Added line #L225 was not covered by tests
case "discord": {
const authToken = await loginWithOauth({
authOption: strategy,
Expand Down
1 change: 1 addition & 0 deletions packages/thirdweb/src/wallets/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const socialAuthOptions = [
"apple",
"facebook",
"discord",
"farcaster",
] as const;
export type SocialAuthOption = (typeof socialAuthOptions)[number];

Expand Down

0 comments on commit 4fab8e8

Please sign in to comment.