Skip to content

Commit

Permalink
Credits funding for slot (#1272)
Browse files Browse the repository at this point in the history
* Credits funding for slot

* load controller from store if not in iframe

* update snapshots

* conditional description
  • Loading branch information
broody authored Jan 15, 2025
1 parent 4a39deb commit 383f106
Show file tree
Hide file tree
Showing 11 changed files with 86 additions and 44 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/keychain/src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Success } from "./success";
import { Pending } from "./pending";
import { Consent, Slot } from "./slot";
import { OcclusionDetector } from "./OcclusionDetector";
import { Fund } from "./slot/fund";

export function App() {
return (
Expand All @@ -19,6 +20,7 @@ export function App() {
<Route path="session" element={<Session />} />
<Route path="slot" element={<Slot />}>
<Route path="consent" element={<Consent />} />
<Route path="fund" element={<Fund />} />
</Route>
<Route path="success" element={<Success />} />
<Route path="failure" element={<Failure />} />
Expand Down
5 changes: 4 additions & 1 deletion packages/keychain/src/components/funding/Balance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import {
} from "@cartridge/utils";
import { useController } from "@/hooks/controller";

export type BalanceType = "credits" | "eth" | "strk";

type BalanceProps = {
showBalances: ("credits" | "eth" | "strk")[];
showBalances: BalanceType[];
};

export function Balance({ showBalances }: BalanceProps) {
Expand All @@ -26,6 +28,7 @@ export function Balance({ showBalances }: BalanceProps) {
username: controller?.username(),
interval: 3000,
});

const {
data: [eth],
} = useERC20Balance({
Expand Down
31 changes: 10 additions & 21 deletions packages/keychain/src/components/funding/PurchaseCredits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Elements } from "@stripe/react-stripe-js";
import { Appearance, loadStripe } from "@stripe/stripe-js";
import { Balance } from "./Balance";
import CheckoutForm from "./StripeCheckout";
import { isIframe } from "@cartridge/utils";

const STRIPE_API_PUBKEY =
"pk_test_51Kr6IXIS6lliDpf33KnwWDtIjRPWt3eAI9CuSLR6Vvc3GxHEwmSU0iszYbUlgUadSRluGKAFphe3JzltyjPAKiBK00al4RAFQu";
Expand All @@ -25,10 +26,11 @@ enum PurchaseState {
}

type PurchaseCreditsProps = {
isSlot?: boolean;
onBack?: () => void;
};

export function PurchaseCredits({ onBack }: PurchaseCreditsProps) {
export function PurchaseCredits({ isSlot, onBack }: PurchaseCreditsProps) {
const { controller, closeModal } = useConnection();

const [clientSecret, setClientSecret] = useState("");
Expand Down Expand Up @@ -82,24 +84,6 @@ export function PurchaseCredits({ onBack }: PurchaseCreditsProps) {
},
} as Appearance;

// For when we need to support Payment Links
// useStripePaymentQuery(
// { referenceId },
// {
// enabled: !!referenceId && !error,
// refetchInterval: REFETCH_INTERVAL,
// retry: MAX_RETRIES,
// onSuccess: () => setState(PurchaseState.SUCCESS),
// onError: () => {
// setError(
// new Error(
// `Payment not received. Please try again. Reference ID: ${referenceId}`,
// ),
// );
// },
// },
// );

if (state === PurchaseState.STRIPE_CHECKOUT) {
return (
<Elements
Expand Down Expand Up @@ -130,13 +114,18 @@ export function PurchaseCredits({ onBack }: PurchaseCreditsProps) {
)
}
onBack={state === PurchaseState.SELECTION ? onBack : undefined}
hideNetwork
>
<Content className="gap-6">
<Balance showBalances={["credits"]} />
<ErrorAlert
variant=""
title="WHAT ARE CREDITS"
description="Credits can be used to play games. They are not tokens and cannot be transferred or refunded."
description={
"Credits can be used " +
(isSlot ? "for slot deployments" : "to play games") +
". They are not tokens and cannot be transferred or refunded."
}
isExpanded
/>
</Content>
Expand All @@ -150,7 +139,7 @@ export function PurchaseCredits({ onBack }: PurchaseCreditsProps) {
/>
)}

{state === PurchaseState.SUCCESS && (
{state === PurchaseState.SUCCESS && isIframe() && (
<Button variant="secondary" onClick={closeModal}>
Close
</Button>
Expand Down
33 changes: 21 additions & 12 deletions packages/keychain/src/components/funding/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "@cartridge/ui-next";
import { DepositEth } from "./DepositEth";
import { PurchaseCredits } from "./PurchaseCredits";
import { Balance } from "./Balance";
import { Balance, BalanceType } from "./Balance";

const enum FundingState {
SHOW_OPTIONS,
Expand All @@ -19,15 +19,18 @@ const enum FundingState {
}

export type FundingProps = {
title?: React.ReactElement;
title?: React.ReactElement | string;
isSlot?: boolean;
onComplete?: (deployHash?: string) => void;
};

export function Funding({ onComplete, title }: FundingProps) {
export function Funding({ title, isSlot, onComplete }: FundingProps) {
const { controller } = useConnection();
const [state, setState] = useState<FundingState>(FundingState.SHOW_OPTIONS);
const showBalances: BalanceType[] = isSlot ? ["credits"] : ["credits", "eth"];
const showCredits =
typeof document !== "undefined" && document.cookie.includes("credits=");
(typeof document !== "undefined" && document.cookie.includes("credits=")) ||
isSlot;

if (state === FundingState.FUND_ETH) {
return (
Expand All @@ -40,7 +43,10 @@ export function Funding({ onComplete, title }: FundingProps) {

if (state === FundingState.FUND_CREDITS) {
return (
<PurchaseCredits onBack={() => setState(FundingState.SHOW_OPTIONS)} />
<PurchaseCredits
isSlot={isSlot}
onBack={() => setState(FundingState.SHOW_OPTIONS)}
/>
);
}

Expand All @@ -49,22 +55,25 @@ export function Funding({ onComplete, title }: FundingProps) {
title={title || (controller ? `Fund ${controller.username()}` : "")}
description={controller && <CopyAddress address={controller.address} />}
icon={<ArrowIcon variant="down" />}
hideNetwork
>
<Content className="gap-6">
<Balance showBalances={["credits", "eth"]} />
<Balance showBalances={showBalances} />
</Content>
<Footer>
{showCredits && (
<Button onClick={() => setState(FundingState.FUND_CREDITS)}>
<CoinsIcon variant="line" size="sm" /> Purchase Credits
</Button>
)}
<Button
onClick={() => setState(FundingState.FUND_ETH)}
variant="secondary"
>
<EthereumIcon size="sm" className="mr-1" /> Deposit Eth
</Button>
{!isSlot && (
<Button
onClick={() => setState(FundingState.FUND_ETH)}
variant="secondary"
>
<EthereumIcon size="sm" className="mr-1" /> Deposit Eth
</Button>
)}
</Footer>
</Container>
);
Expand Down
16 changes: 13 additions & 3 deletions packages/keychain/src/components/slot/consent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import Controller from "@/utils/controller";
import { Button } from "@cartridge/ui-next";
import { Container, Footer } from "@/components/layout";
import { useCallback, useEffect } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";

export function Consent() {
const navigate = useNavigate();
const { pathname } = useLocation();
const [searchParams] = useSearchParams();
const callback_uri = searchParams.get("callback_uri")!;

Expand All @@ -25,9 +26,18 @@ export function Consent() {

useEffect(() => {
if (!Controller.fromStore(import.meta.env.VITE_ORIGIN!)) {
navigate("/slot", { replace: true });
navigate(
`/slot?returnTo=${encodeURIComponent(pathname)}${
callback_uri
? `&callback_uri=${encodeURIComponent(callback_uri)}`
: ""
}`,
{
replace: true,
},
);
}
}, [navigate]);
}, [navigate, callback_uri, pathname]);

return (
<Container
Expand Down
19 changes: 19 additions & 0 deletions packages/keychain/src/components/slot/fund.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect } from "react";
import { Funding } from "../funding";
import Controller from "@/utils/controller";
import { useLocation, useNavigate } from "react-router-dom";

export function Fund() {
const navigate = useNavigate();
const { pathname } = useLocation();

useEffect(() => {
if (!Controller.fromStore(import.meta.env.VITE_ORIGIN!)) {
navigate(`/slot?returnTo=${encodeURIComponent(pathname)}`, {
replace: true,
});
}
}, [navigate, pathname]);

return <Funding title="Fund Credits for Slot" isSlot />;
}
17 changes: 11 additions & 6 deletions packages/keychain/src/components/slot/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function Slot() {
case "/slot/auth/failure":
return <Navigate to="/failure" replace />;
case "/slot/consent":
case "/slot/fund":
return <Outlet />;
default:
return <Auth />;
Expand All @@ -37,13 +38,17 @@ function Auth() {

useEffect(() => {
if (user && controller) {
const query = Array.from(searchParams.entries()).reduce(
(prev, [key, val], i) =>
i === 0 ? `?${key}=${val}` : `${prev}&${key}=${val}`,
"",
);
const returnTo = searchParams.get("returnTo");
const otherParams = Array.from(searchParams.entries())
.filter(([key]) => key !== "returnTo")
.reduce(
(prev, [key, val], i) =>
i === 0 ? `?${key}=${val}` : `${prev}&${key}=${val}`,
"",
);

navigate(`/slot/consent${query}`, { replace: true });
const target = returnTo ? `${returnTo}${otherParams}` : "/slot";
navigate(target, { replace: true });
}
}, [user, controller, navigate, searchParams]);

Expand Down
7 changes: 6 additions & 1 deletion packages/keychain/src/hooks/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,13 @@ export function useConnectionValue() {
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);

// Set rpc and origin if we're not embedded (eg Slot auth/session)
// if we're not embedded (eg Slot auth/session) load controller from store and set origin/rpcUrl
if (!isIframe()) {
const controller = Controller.fromStore(import.meta.env.VITE_ORIGIN!);
if (controller) {
setController(controller);
}

setOrigin(urlParams.get("origin") || import.meta.env.VITE_ORIGIN);
const rpcUrl = urlParams.get("rpc_url");
if (rpcUrl) {
Expand Down

0 comments on commit 383f106

Please sign in to comment.