Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/cli/src/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,14 @@ cli.command(
),
);
cli.command(
createShippingAddressCli(() => factory.createShippingAddressResource()),
createShippingAddressCli(
() => factory.createShippingAddressResource(),
authStorage,
),
);
cli.command(
createUserInfoCli(() => factory.createUserInfoResource(), authStorage),
);
cli.command(createUserInfoCli(() => factory.createUserInfoResource()));
cli.command(createMppCli(spendRequestRepo, authStorage));
cli.command(
createDemoCli(
Expand Down
45 changes: 24 additions & 21 deletions packages/cli/src/commands/auth/logout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { type AuthStorage, storage as defaultStorage } from '@stripe/link-sdk';
import { Box, Text } from 'ink';
import Spinner from 'ink-spinner';
import type React from 'react';
import { useEffect, useState } from 'react';
import { useCallback } from 'react';
import type { IAuthResource } from '../../auth/types';
import { DISPLAY_DELAY_MS } from '../../utils/constants';
import { useAsyncAction } from '../../hooks/use-async-action';

interface LogoutProps {
authResource: IAuthResource;
Expand All @@ -17,28 +18,30 @@ export const Logout: React.FC<LogoutProps> = ({
onComplete,
}) => {
const storage = authStorage;
const [done, setDone] = useState(false);

useEffect(() => {
const run = async () => {
const auth = storage.getAuth();
if (auth?.refresh_token) {
try {
await authResource.revokeToken(auth.refresh_token);
} catch {
// best-effort: clear local storage regardless
}
const action = useCallback(async () => {
const auth = storage.getAuth();
if (auth?.refresh_token) {
try {
await authResource.revokeToken(auth.refresh_token);
} catch {
// best-effort: clear local storage regardless
}
storage.clearAuth();
storage.deleteConfig();
setDone(true);
setTimeout(onComplete, DISPLAY_DELAY_MS);
};
run();
}, [authResource, onComplete, storage]);
}
storage.clearAuth();
storage.deleteConfig();
}, [authResource, storage]);

if (!done) {
return null;
const { status } = useAsyncAction(action, onComplete);

if (status === 'loading') {
return (
<Box>
<Text color="cyan">
<Spinner type="dots" /> Logging out...
</Text>
</Box>
);
}

return (
Expand Down
17 changes: 12 additions & 5 deletions packages/cli/src/commands/demo/demo-runner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
ISpendRequestResource,
} from '@stripe/link-sdk';
import { storage as defaultStorage } from '@stripe/link-sdk';
import { Box, Text, useInput } from 'ink';
import { Box, Text, useApp, useInput } from 'ink';
import type React from 'react';
import { useCallback, useState } from 'react';
import type { IAuthResource } from '../../auth/types';
Expand Down Expand Up @@ -46,6 +46,7 @@ export const DemoRunner: React.FC<DemoRunnerProps> = ({
onlySpt,
onComplete,
}) => {
const { exit } = useApp();
const storage = authStorage;
const preselected = onlyCard ? 'card' : onlySpt ? 'spt' : null;
const [choice, setChoice] = useState<Choice | null>(preselected);
Expand Down Expand Up @@ -88,21 +89,27 @@ export const DemoRunner: React.FC<DemoRunnerProps> = ({

if (!runSpt) {
setPhase('summary');
setTimeout(onComplete, DISPLAY_DELAY_MS);
setTimeout(() => {
onComplete();
exit();
}, DISPLAY_DELAY_MS);
} else {
setPhase('card-done');
}
},
[runSpt, onComplete],
[runSpt, onComplete, exit],
);

const onSptComplete = useCallback(
(success: boolean) => {
setSptSuccess(success);
setPhase('summary');
setTimeout(onComplete, DISPLAY_DELAY_MS);
setTimeout(() => {
onComplete();
exit();
}, DISPLAY_DELAY_MS);
},
[onComplete],
[onComplete, exit],
);

return (
Expand Down
28 changes: 13 additions & 15 deletions packages/cli/src/commands/demo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import type {
ISpendRequestResource,
} from '@stripe/link-sdk';
import { Cli, z } from 'incur';
import { render } from 'ink';
import React from 'react';
import type { IAuthResource } from '../../auth/types';
import { renderInteractive } from '../../utils/render-interactive';
import { DemoRunner } from './demo-runner';

const demoOptions = z.object({
Expand Down Expand Up @@ -41,20 +41,18 @@ export function createDemoCli(

const paymentMethodsResource = createPaymentMethodsResource();

return new Promise((resolve) => {
const { waitUntilExit, unmount } = render(
<DemoRunner
authRepo={authRepo}
spendRequestRepo={spendRequestRepo}
paymentMethodsResource={paymentMethodsResource}
authStorage={authStorage}
onlyCard={c.options.onlyCard}
onlySpt={c.options.onlySpt}
onComplete={() => unmount()}
/>,
);
waitUntilExit().then(() => resolve({}));
});
return renderInteractive(
<DemoRunner
authRepo={authRepo}
spendRequestRepo={spendRequestRepo}
paymentMethodsResource={paymentMethodsResource}
authStorage={authStorage}
onlyCard={c.options.onlyCard}
onlySpt={c.options.onlySpt}
onComplete={() => {}}
/>,
() => ({}),
);
},
});
}
24 changes: 11 additions & 13 deletions packages/cli/src/commands/onboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import type {
ISpendRequestResource,
} from '@stripe/link-sdk';
import { Cli } from 'incur';
import { render } from 'ink';
import React from 'react';
import type { IAuthResource } from '../../auth/types';
import { renderInteractive } from '../../utils/render-interactive';
import { OnboardRunner } from './onboard-runner';

export function createOnboardCli(
Expand All @@ -29,18 +29,16 @@ export function createOnboardCli(

const paymentMethodsResource = createPaymentMethodsResource();

return new Promise((resolve) => {
const { waitUntilExit, unmount } = render(
<OnboardRunner
authRepo={authRepo}
spendRequestRepo={spendRequestRepo}
paymentMethodsResource={paymentMethodsResource}
authStorage={authStorage}
onComplete={() => unmount()}
/>,
);
waitUntilExit().then(() => resolve({}));
});
return renderInteractive(
<OnboardRunner
authRepo={authRepo}
spendRequestRepo={spendRequestRepo}
paymentMethodsResource={paymentMethodsResource}
authStorage={authStorage}
onComplete={() => {}}
/>,
() => ({}),
);
},
});
}
4 changes: 3 additions & 1 deletion packages/cli/src/commands/onboard/onboard-runner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
ISpendRequestResource,
} from '@stripe/link-sdk';
import { storage as defaultStorage } from '@stripe/link-sdk';
import { Box, Text, useInput } from 'ink';
import { Box, Text, useApp, useInput } from 'ink';
import type React from 'react';
import { useEffect, useRef, useState } from 'react';
import type { IAuthResource } from '../../auth/types';
Expand All @@ -29,6 +29,7 @@ export const OnboardRunner: React.FC<OnboardRunnerProps> = ({
authStorage = defaultStorage,
onComplete,
}) => {
const { exit } = useApp();
const storage = authStorage;
const [phase, setPhase] = useState<Phase>('welcome');
const [authSkipped, setAuthSkipped] = useState(false);
Expand Down Expand Up @@ -90,6 +91,7 @@ export const OnboardRunner: React.FC<OnboardRunnerProps> = ({
setPhase('demo');
} catch (err) {
setError((err as Error).message);
setTimeout(exit, 2000);
}
};
run();
Expand Down
32 changes: 6 additions & 26 deletions packages/cli/src/commands/payment-methods/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,20 @@ import type { IPaymentMethodsResource, PaymentMethod } from '@stripe/link-sdk';
import { Box, Text } from 'ink';
import Spinner from 'ink-spinner';
import type React from 'react';
import { useEffect, useState } from 'react';
import { DISPLAY_DELAY_MS } from '../../utils/constants';
import { useCallback } from 'react';
import { useAsyncAction } from '../../hooks/use-async-action';

interface PaymentMethodsListProps {
resource: IPaymentMethodsResource;
onComplete: () => void;
onComplete: (result: PaymentMethod[] | null) => void;
}

export const PaymentMethodsList: React.FC<PaymentMethodsListProps> = ({
resource,
onComplete,
}) => {
const [status, setStatus] = useState<'loading' | 'success' | 'error'>(
'loading',
);
const [methods, setMethods] = useState<PaymentMethod[]>([]);
const [error, setError] = useState<string>('');

useEffect(() => {
const fetch = async () => {
try {
const result = await resource.listPaymentMethods();
setMethods(result);
setStatus('success');
setTimeout(onComplete, DISPLAY_DELAY_MS);
} catch (err) {
setError((err as Error).message);
setStatus('error');
setTimeout(onComplete, DISPLAY_DELAY_MS);
}
};

fetch();
}, [resource, onComplete]);
const action = useCallback(() => resource.listPaymentMethods(), [resource]);
const { status, data: methods, error } = useAsyncAction(action, onComplete);

if (status === 'loading') {
return (
Expand All @@ -56,7 +36,7 @@ export const PaymentMethodsList: React.FC<PaymentMethodsListProps> = ({
);
}

if (methods.length === 0) {
if (!methods || methods.length === 0) {
return (
<Box>
<Text dimColor>No payment methods found</Text>
Expand Down
32 changes: 9 additions & 23 deletions packages/cli/src/commands/shipping-address/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { IShippingAddressResource } from '@stripe/link-sdk';
import { storage } from '@stripe/link-sdk';
import type { AuthStorage, IShippingAddressResource } from '@stripe/link-sdk';
import { Cli } from 'incur';
import { render } from 'ink';
import React from 'react';
import { renderInteractive } from '../../utils/render-interactive';
import { requireAuth } from '../../utils/require-auth';
import { ShippingAddressList } from './list';

export function createShippingAddressCli(
createResource: () => IShippingAddressResource,
authStorage?: AuthStorage,
) {
const cli = Cli.create('shipping-address', {
description: 'Shipping address management commands',
Expand All @@ -15,30 +16,15 @@ export function createShippingAddressCli(
cli.command('list', {
description: 'List all shipping addresses on your account',
outputPolicy: 'agent-only' as const,
middleware: [requireAuth(authStorage)],
async run(c) {
if (!storage.isAuthenticated()) {
return c.error({
code: 'NOT_AUTHENTICATED',
message: 'Not authenticated. Run "link-cli auth login" first.',
cta: {
commands: [
{ command: 'auth login', description: 'Log in to Link' },
],
},
});
}

const resource = createResource();

if (!c.agent && !c.formatExplicit) {
return new Promise((resolve) => {
const { waitUntilExit } = render(
<ShippingAddressList resource={resource} onComplete={() => {}} />,
);
waitUntilExit().then(async () => {
resolve(await resource.listShippingAddresses());
});
});
return renderInteractive(
<ShippingAddressList resource={resource} onComplete={() => {}} />,
() => resource.listShippingAddresses(),
);
}

return resource.listShippingAddresses();
Expand Down
Loading
Loading