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
17 changes: 14 additions & 3 deletions libs/ui/src/domain/usecases/checkout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ describe('CheckoutUsecase', () => {

checkout.dispatch({ type: 'ASK_DETAILS' });
checkout.dispatch({ type: 'CHANGE_METHOD', method: 'cod' });
checkout.dispatch({ type: 'OPEN_PHOTO_CAPTURE' });
checkout.dispatch({ type: 'CAPTURE_PHOTO', photo: 'base64jpeg' });
checkout.dispatch({ type: 'SUBMIT_DETAILS' });
expect(checkout.state.type).toBe('creatingPayment');
Expand All @@ -444,16 +445,24 @@ describe('CheckoutUsecase', () => {
expect(checkout.state.payment?.method).toBe('cod');
});

it('should clear the captured photo on RETAKE_PHOTO', () => {
it('should move to a dedicated capturingPhoto step and back, keeping the photo already taken', () => {
const repository = new MockPaymentRepository();
const checkout = createTester(repository);

checkout.dispatch({ type: 'ASK_DETAILS' });
checkout.dispatch({ type: 'CHANGE_METHOD', method: 'cod' });
checkout.dispatch({ type: 'OPEN_PHOTO_CAPTURE' });
expect(checkout.state.type).toBe('capturingPhoto');

checkout.dispatch({ type: 'CAPTURE_PHOTO', photo: 'base64jpeg' });
checkout.dispatch({ type: 'RETAKE_PHOTO' });
expect(checkout.state.type).toBe('askingDetails');
expect(checkout.state.verificationPhoto).toBe('base64jpeg');

expect(checkout.state.verificationPhoto).toBeNull();
checkout.dispatch({ type: 'OPEN_PHOTO_CAPTURE' });
checkout.dispatch({ type: 'BACK_TO_DETAILS' });

expect(checkout.state.type).toBe('askingDetails');
expect(checkout.state.verificationPhoto).toBe('base64jpeg');
});

it('should clear the captured photo when the method changes away from cod', () => {
Expand All @@ -462,6 +471,7 @@ describe('CheckoutUsecase', () => {

checkout.dispatch({ type: 'ASK_DETAILS' });
checkout.dispatch({ type: 'CHANGE_METHOD', method: 'cod' });
checkout.dispatch({ type: 'OPEN_PHOTO_CAPTURE' });
checkout.dispatch({ type: 'CAPTURE_PHOTO', photo: 'base64jpeg' });
checkout.dispatch({ type: 'CHANGE_METHOD', method: 'qris' });

Expand Down Expand Up @@ -495,6 +505,7 @@ describe('CheckoutUsecase', () => {

checkout.dispatch({ type: 'ASK_DETAILS' });
checkout.dispatch({ type: 'CHANGE_METHOD', method: 'cod' });
checkout.dispatch({ type: 'OPEN_PHOTO_CAPTURE' });
checkout.dispatch({ type: 'CAPTURE_PHOTO', photo: 'base64jpeg' });
checkout.dispatch({ type: 'SUBMIT_DETAILS' });
await flushMicrotasks();
Expand Down
20 changes: 15 additions & 5 deletions libs/ui/src/domain/usecases/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Context = {
export type CheckoutState = (
| { type: 'idle' }
| { type: 'askingDetails' }
| { type: 'capturingPhoto' }
| { type: 'creatingPayment' }
| { type: 'created' }
| { type: 'error' }
Expand All @@ -41,8 +42,9 @@ export type CheckoutAction =
| { type: 'CHANGE_WHATSAPP_NUMBER'; whatsappNumber: string }
| { type: 'CHANGE_METHOD'; method: PaymentMethod }
| { type: 'CHANGE_DINING_OPTION'; diningOption: PaymentDiningOption }
| { type: 'OPEN_PHOTO_CAPTURE' }
| { type: 'CAPTURE_PHOTO'; photo: string }
| { type: 'RETAKE_PHOTO' }
| { type: 'BACK_TO_DETAILS' }
| { type: 'CANCEL_DETAILS' }
| { type: 'SUBMIT_DETAILS' }
| { type: 'CHECKOUT_SUCCESS'; payment: Payment }
Expand Down Expand Up @@ -161,12 +163,20 @@ export class CheckoutUsecase extends Usecase<
([state, { diningOption }]) => ({ ...state, diningOption })
)
.with(
[{ type: 'askingDetails' }, { type: 'CAPTURE_PHOTO' }],
([state, { photo }]) => ({ ...state, verificationPhoto: photo })
[{ type: 'askingDetails' }, { type: 'OPEN_PHOTO_CAPTURE' }],
([state]) => ({ ...state, type: 'capturingPhoto' })
)
.with(
[{ type: 'askingDetails' }, { type: 'RETAKE_PHOTO' }],
([state]) => ({ ...state, verificationPhoto: null })
[{ type: 'capturingPhoto' }, { type: 'CAPTURE_PHOTO' }],
([state, { photo }]) => ({
...state,
type: 'askingDetails',
verificationPhoto: photo,
})
)
.with(
[{ type: 'capturingPhoto' }, { type: 'BACK_TO_DETAILS' }],
([state]) => ({ ...state, type: 'askingDetails' })
)
.with(
[{ type: 'askingDetails' }, { type: 'CANCEL_DETAILS' }],
Expand Down
33 changes: 33 additions & 0 deletions libs/ui/src/presentation/handlers/order/CartHandler.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,9 @@ describe('CartHandler', () => {
).disabled
).toBe(true);

await user.click(
screen.getByRole('button', { name: 'Ambil foto verifikasi' })
);
await capturePhoto(user, container);

const submitButton = screen.getByRole('button', {
Expand All @@ -661,6 +664,36 @@ describe('CartHandler', () => {
`/orders/${paymentRepository.payment.reference}`
);
});

it('opens a dedicated photo capture step, and going back leaves the details sheet without a photo', async () => {
stubCamera();
const user = userEvent.setup();
const cartRepository = new MockCartRepository();
await addItemToCart(cartRepository);
renderHandler({
cartRepository,
customerName: 'Budi',
customerWhatsappNumber: '081234567890',
enabledMethods: ['qris', 'cod'],
});
await settle();

await user.click(screen.getByRole('button', { name: payButtonName }));
await user.click(screen.getByLabelText('Bayar dengan COD'));
await user.click(
screen.getByRole('button', { name: 'Ambil foto verifikasi' })
);

expect(screen.getByText('Foto verifikasi')).toBeTruthy();
expect(screen.queryByPlaceholderText('Nama Anda')).toBeNull();

await user.click(screen.getByRole('button', { name: 'Kembali' }));

expect(screen.getByPlaceholderText('Nama Anda')).toBeTruthy();
expect(
screen.getByRole('button', { name: 'Ambil foto verifikasi' })
).toBeTruthy();
});
});

it('submits directly when the sheet opens prefilled with a valid name and WhatsApp number', async () => {
Expand Down
15 changes: 13 additions & 2 deletions libs/ui/src/presentation/handlers/order/CartHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useCart } from '../hooks/useCart';
import { useCheckout } from '../hooks/useCheckout';
import { usePaymentCancel } from '../hooks/usePaymentCancel';
import { useTableResolve } from '../hooks/useTableResolve';
import { CodPhotoCaptureSheetProps } from '../../views/components/checkout/CodPhotoCaptureSheet';
import { CustomerDetailsSheetProps } from '../../views/components/checkout/CustomerDetailsSheet';
import { CartItemEditScreenProps } from '../../views/screens/order/CartItemEditScreen';
import {
Expand Down Expand Up @@ -241,10 +242,19 @@ export const CartHandler = ({
checkout.dispatch({ type: 'CHANGE_DINING_OPTION', diningOption }),
cashierLocation,
verificationPhoto: checkout.state.verificationPhoto,
onOpenPhotoCapture: () =>
checkout.dispatch({ type: 'OPEN_PHOTO_CAPTURE' }),
isSubmitting: isSubmittingDetails,
}
: null;

const photoCapture: (CodPhotoCaptureSheetProps & { isOpen: true }) | null =
checkout.state.type === 'capturingPhoto'
? {
isOpen: true,
onCapturePhoto: (photo) =>
checkout.dispatch({ type: 'CAPTURE_PHOTO', photo }),
onRetakePhoto: () => checkout.dispatch({ type: 'RETAKE_PHOTO' }),
isSubmitting: isSubmittingDetails,
onBackPress: () => checkout.dispatch({ type: 'BACK_TO_DETAILS' }),
}
: null;

Expand Down Expand Up @@ -311,6 +321,7 @@ export const CartHandler = ({
onCheckoutPress={() => checkout.dispatch({ type: 'ASK_DETAILS' })}
onCheckoutRetryPress={() => checkout.dispatch({ type: 'SUBMIT_DETAILS' })}
detailsSheet={detailsSheet}
photoCapture={photoCapture}
lockedNotice={lockedNotice}
cancelConfirmation={cancelConfirmation}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type CameraState =
| { type: 'denied' }
| { type: 'unsupported' };

const VIEWFINDER_SIZE = 280;
const VIEWFINDER_ASPECT_RATIO = 3 / 4;
const MAX_CAPTURE_DIMENSION = 1280;
const JPEG_QUALITY = 0.7;

Expand Down Expand Up @@ -144,8 +144,8 @@ export const CameraCapture = ({ onCapture }: CameraCaptureProps) => {
<YStack gap="$3" alignItems="center">
<Image
src={`data:image/jpeg;base64,${state.photo}`}
width={VIEWFINDER_SIZE}
height={VIEWFINDER_SIZE * 0.75}
width="100%"
aspectRatio={VIEWFINDER_ASPECT_RATIO}
borderRadius="$4"
/>
<XStack gap="$3">
Expand All @@ -164,8 +164,8 @@ export const CameraCapture = ({ onCapture }: CameraCaptureProps) => {
<YStack gap="$3" alignItems="center">
<Paragraph textAlign="center">{GUIDANCE_TEXT}</Paragraph>
<YStack
width={VIEWFINDER_SIZE}
height={VIEWFINDER_SIZE * 0.75}
width="100%"
aspectRatio={VIEWFINDER_ASPECT_RATIO}
borderRadius="$4"
overflow="hidden"
backgroundColor="$color2"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { CodPhotoCaptureSheet } from './CodPhotoCaptureSheet';

function stubGetUserMedia(impl: () => Promise<MediaStream>) {
Object.defineProperty(navigator, 'mediaDevices', {
configurable: true,
value: { getUserMedia: impl },
});
}

const meta: Meta<typeof CodPhotoCaptureSheet> = {
title: 'Components/Checkout/CodPhotoCaptureSheet',
component: CodPhotoCaptureSheet,
args: {
isOpen: true,
onCapturePhoto: fn(),
onBackPress: fn(),
},
};

export default meta;
type Story = StoryObj<typeof CodPhotoCaptureSheet>;

export const Requesting: Story = {
render: (args) => {
stubGetUserMedia(() => new Promise<MediaStream>(() => undefined));
return <CodPhotoCaptureSheet {...args} />;
},
};

export const Closed: Story = {
args: { isOpen: false },
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Button, Paragraph, ScrollView, XStack, YStack } from 'tamagui';
import { ChevronLeft } from '@tamagui/lucide-icons';
import { Sheet } from '../base/Sheet';
import { CameraCapture } from '../base/CameraCapture';

export type CodPhotoCaptureSheetProps = {
isOpen: boolean;
onCapturePhoto: (photo: string) => void;
onBackPress: () => void;
};

export const CodPhotoCaptureSheet = ({
isOpen,
onCapturePhoto,
onBackPress,
}: CodPhotoCaptureSheetProps) => {
return (
<Sheet isOpen={isOpen} onOpenChange={(open) => !open && onBackPress()}>
<YStack flex={1}>
<XStack padding="$4" paddingBottom="$2" alignItems="center" gap="$2">
<Button
icon={ChevronLeft}
chromeless
size="$3"
onPress={onBackPress}
accessibilityLabel="Kembali"
/>
<Paragraph fontWeight="bold" fontSize="$6">
Foto verifikasi
</Paragraph>
</XStack>

<ScrollView flex={1}>
<YStack padding="$4" paddingTop="$0">
<CameraCapture onCapture={onCapturePhoto} />
</YStack>
</ScrollView>
</YStack>
</Sheet>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ const meta: Meta<typeof CustomerDetailsSheet> = {
},
cashierLocation: 'Lantai 1',
verificationPhoto: null,
onCapturePhoto: () => {
// Storybook action stand-in
},
onRetakePhoto: () => {
onOpenPhotoCapture: () => {
// Storybook action stand-in
},
isSubmitting: false,
Expand Down
Loading
Loading