Skip to content

Commit 49e7bab

Browse files
feat(pos-app): migrate transactions API to unified Pay API
Migrate from old Merchant API (`/merchants/{id}/payments` with separate `MERCHANT_PORTAL_API_KEY`) to unified Pay API (`/v1/merchants/payments` with shared `getApiHeaders()`). Update response types from flat snake_case to nested camelCase DTOs. Replace CAIP-19 token parsing with server-provided display values. Remove `utils/tokens.ts` and merchant API env vars. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8e5afb8 commit 49e7bab

File tree

14 files changed

+152
-316
lines changed

14 files changed

+152
-316
lines changed

dapps/pos-app/.env.example

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,3 @@ EXPO_PUBLIC_API_URL=""
55
EXPO_PUBLIC_GATEWAY_URL=""
66
EXPO_PUBLIC_DEFAULT_MERCHANT_ID=""
77
EXPO_PUBLIC_DEFAULT_CUSTOMER_API_KEY=""
8-
EXPO_PUBLIC_MERCHANT_API_URL=""
9-
EXPO_PUBLIC_MERCHANT_PORTAL_API_KEY=""

dapps/pos-app/AGENTS.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ The app uses **Zustand** for state management with two main stores:
7979
- Biometric authentication settings
8080
- Printer connection status
8181
- Transaction filter preference (for Activity screen)
82+
- Date range filter preference (for Activity screen)
8283

8384
2. **`useLogsStore`** (`store/useLogsStore.ts`)
8485
- Debug logs for troubleshooting
@@ -220,20 +221,19 @@ All Payment API requests include:
220221

221222
### Transactions Service (`services/transactions.ts`)
222223

223-
> **Note:** The Merchants API currently has its own auth layer separate from the Payment API. Both share the same base URL (`EXPO_PUBLIC_API_URL`), but merchant endpoints authenticate via `EXPO_PUBLIC_MERCHANT_PORTAL_API_KEY` (sent as `x-api-key` header) rather than the partner API key used by payment endpoints. This will be unified in the future.
224-
225224
**`getTransactions(options)`**
226225

227226
- Fetches merchant transaction history
228-
- Endpoint: `GET /merchants/{merchant_id}/payments`
229-
- Uses the shared base URL (`EXPO_PUBLIC_API_URL`) but authenticates with `EXPO_PUBLIC_MERCHANT_PORTAL_API_KEY`
230-
- Supports filtering by status, date range, pagination
231-
- Returns array of `PaymentRecord` objects
227+
- Endpoint: `GET /v1/merchants/payments`
228+
- Uses `getApiHeaders()` for authentication (same as payment endpoints)
229+
- Supports filtering by status, date range (`startTs`/`endTs`), pagination (`cursor`/`limit`)
230+
- Returns `TransactionsResponse` with nested camelCase DTOs (`PaymentRecord`, `AmountWithDisplay`, `BuyerInfo`, `TransactionInfo`, `SettlementInfo`)
232231

233232
### Server-Side Proxy (`api/transactions.ts`)
234233

235234
- Vercel serverless function that proxies transaction requests (web only)
236-
- Client only sends `x-merchant-id` header; API key is handled server-side via `EXPO_PUBLIC_MERCHANT_PORTAL_API_KEY`
235+
- Uses shared `extractCredentials()` and `getApiHeaders()` from `api/_utils.ts`
236+
- Client sends `x-api-key` and `x-merchant-id` headers; proxy forwards with full auth headers
237237
- Avoids CORS issues by making requests server-side
238238

239239
### useTransactions Hook (`services/hooks.ts`)
@@ -242,7 +242,8 @@ All Payment API requests include:
242242
import { useTransactions } from "@/services/hooks";
243243

244244
const { data, isLoading, isError, refetch } = useTransactions({
245-
filter: "all", // "all" | "completed" | "pending" | "failed"
245+
filter: "all", // "all" | "pending" | "completed" | "failed" | "expired" | "cancelled"
246+
dateRangeFilter: "today", // "all_time" | "today" | "7_days" | "this_week" | "this_month"
246247
enabled: true,
247248
});
248249
```
@@ -264,8 +265,6 @@ EXPO_PUBLIC_API_URL="" # Payment API base URL
264265
EXPO_PUBLIC_GATEWAY_URL="" # WalletConnect gateway URL
265266
EXPO_PUBLIC_DEFAULT_MERCHANT_ID="" # Default merchant ID (optional)
266267
EXPO_PUBLIC_DEFAULT_CUSTOMER_API_KEY="" # Default customer API key (optional)
267-
EXPO_PUBLIC_MERCHANT_API_URL="" # Merchant Portal API base URL
268-
EXPO_PUBLIC_MERCHANT_PORTAL_API_KEY="" # Merchant Portal API key (for Activity screen)
269268
```
270269

271270
Copy `.env.example` to `.env` and fill in values.

dapps/pos-app/api/transactions.ts

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import type { VercelRequest, VercelResponse } from "@vercel/node";
2-
3-
const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL;
4-
// TODO: Once Merchants API unifies auth with Payment API, forward client credentials instead
5-
const MERCHANT_PORTAL_API_KEY = process.env.EXPO_PUBLIC_MERCHANT_PORTAL_API_KEY;
2+
import { extractCredentials, getApiBaseUrl, getApiHeaders } from "./_utils";
63

74
/**
85
* Vercel Serverless Function to proxy transaction list requests
@@ -17,30 +14,16 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
1714
}
1815

1916
try {
20-
// Extract merchant ID from request headers
21-
const merchantId = req.headers["x-merchant-id"] as string;
22-
23-
if (!merchantId) {
24-
return res.status(400).json({
25-
message: "Missing required header: x-merchant-id",
26-
});
27-
}
28-
29-
if (!API_BASE_URL) {
30-
return res.status(500).json({
31-
message: "API_BASE_URL is not configured",
32-
});
33-
}
17+
const credentials = extractCredentials(req, res);
18+
if (!credentials) return;
3419

35-
if (!MERCHANT_PORTAL_API_KEY) {
36-
return res.status(500).json({
37-
message: "MERCHANT_PORTAL_API_KEY is not configured",
38-
});
39-
}
20+
const apiBaseUrl = getApiBaseUrl(res);
21+
if (!apiBaseUrl) return;
4022

41-
// Build query string from request query params
23+
// Forward query params as-is (already camelCase from client)
4224
const params = new URLSearchParams();
43-
const { status, sort_by, sort_dir, limit, cursor } = req.query;
25+
const { status, sortBy, sortDir, limit, cursor, startTs, endTs } =
26+
req.query;
4427

4528
// Handle status (can be array for multiple status filters)
4629
if (status) {
@@ -50,29 +33,32 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
5033
params.append("status", status);
5134
}
5235
}
53-
if (sort_by && typeof sort_by === "string") {
54-
params.append("sort_by", sort_by);
36+
if (sortBy && typeof sortBy === "string") {
37+
params.append("sortBy", sortBy);
5538
}
56-
if (sort_dir && typeof sort_dir === "string") {
57-
params.append("sort_dir", sort_dir);
39+
if (sortDir && typeof sortDir === "string") {
40+
params.append("sortDir", sortDir);
5841
}
5942
if (limit && typeof limit === "string") {
6043
params.append("limit", limit);
6144
}
6245
if (cursor && typeof cursor === "string") {
6346
params.append("cursor", cursor);
6447
}
48+
if (startTs && typeof startTs === "string") {
49+
params.append("startTs", startTs);
50+
}
51+
if (endTs && typeof endTs === "string") {
52+
params.append("endTs", endTs);
53+
}
6554

6655
const queryString = params.toString();
67-
const normalizedBaseUrl = API_BASE_URL.replace(/\/+$/, "");
68-
const endpoint = `/merchants/${encodeURIComponent(merchantId)}/payments${queryString ? `?${queryString}` : ""}`;
56+
const normalizedBaseUrl = apiBaseUrl.replace(/\/+$/, "");
57+
const endpoint = `/v1/merchants/payments${queryString ? `?${queryString}` : ""}`;
6958

7059
const response = await fetch(`${normalizedBaseUrl}${endpoint}`, {
7160
method: "GET",
72-
headers: {
73-
"Content-Type": "application/json",
74-
"x-api-key": MERCHANT_PORTAL_API_KEY,
75-
},
61+
headers: getApiHeaders(credentials.apiKey, credentials.merchantId),
7662
});
7763

7864
const data = await response.json();

dapps/pos-app/app/activity.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,7 @@ export default function ActivityScreen() {
156156
[handleTransactionPress],
157157
);
158158

159-
const keyExtractor = useCallback(
160-
(item: PaymentRecord) => item.payment_id,
161-
[],
162-
);
159+
const keyExtractor = useCallback((item: PaymentRecord) => item.paymentId, []);
163160

164161
const renderEmptyComponent = useCallback(() => {
165162
if (isLoading) {

dapps/pos-app/app/payment-success.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,7 @@ export default function PaymentSuccessScreen() {
212212
onPress={handleNewPayment}
213213
>
214214
<ThemedText
215-
style={[
216-
styles.buttonText,
217-
{ color: DarkTheme["text-primary"] },
218-
]}
215+
style={[styles.buttonText, { color: DarkTheme["text-primary"] }]}
219216
>
220217
New payment
221218
</ThemedText>

dapps/pos-app/components/transaction-card.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,18 @@ function TransactionCardBase({
3333
>
3434
<View style={styles.leftContent}>
3535
<ThemedText fontSize={16} lineHeight={20} color="text-primary">
36-
{formatFiatAmount(payment.fiat_amount, payment.fiat_currency)}
36+
{formatFiatAmount(
37+
payment.fiatAmount?.value,
38+
payment.fiatAmount?.unit,
39+
)}
3740
</ThemedText>
3841
<ThemedText
3942
fontSize={14}
4043
lineHeight={18}
4144
color="text-secondary"
4245
style={styles.date}
4346
>
44-
{formatShortDate(payment.created_at)}
47+
{formatShortDate(payment.createdAt)}
4548
</ThemedText>
4649
</View>
4750
<StatusBadge status={payment.status} />

dapps/pos-app/components/transaction-detail-modal.tsx

Lines changed: 20 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { BorderRadius, Spacing } from "@/constants/spacing";
22
import { useTheme } from "@/hooks/use-theme-color";
33
import { formatFiatAmount } from "@/utils/currency";
44
import { formatDateTime } from "@/utils/misc";
5-
import { formatCryptoReceived, getTokenSymbol } from "@/utils/tokens";
65
import { PaymentRecord } from "@/utils/types";
76
import { memo, useEffect } from "react";
87
import {
@@ -46,23 +45,6 @@ function truncateHash(hash?: string): string {
4645
return `${hash.slice(0, 4)}...${hash.slice(-4)}`;
4746
}
4847

49-
/**
50-
* Get the token icon based on CAIP-19 identifier
51-
* Returns the icon source or null if not USDC/USDT
52-
*/
53-
function getTokenIcon(tokenCaip19?: string): number | null {
54-
const symbol = getTokenSymbol(tokenCaip19);
55-
if (!symbol) return null;
56-
57-
if (symbol === "USDC") {
58-
return require("@/assets/images/tokens/usdc.png");
59-
}
60-
if (symbol === "USDT") {
61-
return require("@/assets/images/tokens/usdt.png");
62-
}
63-
return null;
64-
}
65-
6648
interface DetailRowProps {
6749
label: string;
6850
value?: string;
@@ -142,14 +124,15 @@ function TransactionDetailModalBase({
142124
if (!payment) return null;
143125

144126
const handleCopyPaymentId = async () => {
145-
if (!payment?.payment_id) return;
146-
await Clipboard.setStringAsync(payment.payment_id);
127+
if (!payment?.paymentId) return;
128+
await Clipboard.setStringAsync(payment.paymentId);
147129
showSuccessToast("Payment ID copied to clipboard");
148130
};
149131

132+
const txHash = payment.transaction?.hash;
150133
const handleCopyHash = async () => {
151-
if (!payment?.tx_hash) return;
152-
await Clipboard.setStringAsync(payment.tx_hash);
134+
if (!txHash) return;
135+
await Clipboard.setStringAsync(txHash);
153136
showSuccessToast("Transaction hash copied to clipboard");
154137
};
155138

@@ -190,7 +173,7 @@ function TransactionDetailModalBase({
190173
<View style={styles.details}>
191174
<DetailRow
192175
label="Date"
193-
value={formatDateTime(payment.created_at)}
176+
value={formatDateTime(payment.createdAt)}
194177
/>
195178

196179
<DetailRow label="Status">
@@ -200,45 +183,43 @@ function TransactionDetailModalBase({
200183
<DetailRow
201184
label="Amount"
202185
value={formatFiatAmount(
203-
payment.fiat_amount,
204-
payment.fiat_currency,
186+
payment.fiatAmount?.value,
187+
payment.fiatAmount?.unit,
205188
)}
206189
/>
207190

208-
{payment.token_amount && payment.token_caip19 && (
191+
{payment.tokenAmount?.value && (
209192
<DetailRow label="Crypto received">
210193
<View style={styles.cryptoValue}>
211194
<ThemedText
212195
fontSize={16}
213196
lineHeight={18}
214197
color="text-primary"
215198
>
216-
{formatCryptoReceived(
217-
payment.token_caip19,
218-
payment.token_amount,
219-
) ?? payment.token_amount}
199+
{payment.tokenAmount.display?.formatted ??
200+
payment.tokenAmount.value}
220201
</ThemedText>
221-
{(() => {
222-
const icon = getTokenIcon(payment.token_caip19);
223-
return icon ? (
224-
<Image style={styles.tokenIcon} source={icon} />
225-
) : null;
226-
})()}
202+
{payment.tokenAmount.display?.iconUrl && (
203+
<Image
204+
style={styles.tokenIcon}
205+
source={{ uri: payment.tokenAmount.display.iconUrl }}
206+
/>
207+
)}
227208
</View>
228209
</DetailRow>
229210
)}
230211

231212
<DetailRow
232213
label="Payment ID"
233-
value={payment.payment_id}
214+
value={payment.paymentId}
234215
onPress={handleCopyPaymentId}
235216
underline
236217
/>
237218

238-
{payment.tx_hash && (
219+
{txHash && (
239220
<DetailRow
240221
label="Hash ID"
241-
value={truncateHash(payment.tx_hash)}
222+
value={truncateHash(txHash)}
242223
onPress={handleCopyHash}
243224
underline
244225
/>

dapps/pos-app/services/hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export function useTransactions(options: UseTransactionsOptions = {}) {
238238
});
239239
},
240240
initialPageParam: undefined as string | undefined,
241-
getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined,
241+
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
242242
enabled,
243243
staleTime: 5 * 60 * 1000, // 5 minutes
244244
gcTime: 30 * 60 * 1000, // 30 minutes (formerly cacheTime)

dapps/pos-app/services/transactions.ts

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import { useSettingsStore } from "@/store/useSettingsStore";
21
import { TransactionsResponse } from "@/utils/types";
3-
// TODO: Once Merchants API unifies auth with Payment API, switch to getApiHeaders()
4-
import { apiClient } from "./client";
5-
6-
const MERCHANT_PORTAL_API_KEY = process.env.EXPO_PUBLIC_MERCHANT_PORTAL_API_KEY;
2+
import { apiClient, getApiHeaders } from "./client";
73

84
export interface GetTransactionsOptions {
95
status?: string | string[];
@@ -23,15 +19,7 @@ export interface GetTransactionsOptions {
2319
export async function getTransactions(
2420
options: GetTransactionsOptions = {},
2521
): Promise<TransactionsResponse> {
26-
const merchantId = useSettingsStore.getState().merchantId;
27-
28-
if (!merchantId) {
29-
throw new Error("Merchant ID is not configured");
30-
}
31-
32-
if (!MERCHANT_PORTAL_API_KEY) {
33-
throw new Error("Merchant Portal API key is not configured");
34-
}
22+
const headers = await getApiHeaders();
3523

3624
// Build query string from options
3725
const params = new URLSearchParams();
@@ -45,11 +33,11 @@ export async function getTransactions(
4533
}
4634

4735
if (options.sortBy) {
48-
params.append("sort_by", options.sortBy);
36+
params.append("sortBy", options.sortBy);
4937
}
5038

5139
if (options.sortDir) {
52-
params.append("sort_dir", options.sortDir);
40+
params.append("sortDir", options.sortDir);
5341
}
5442

5543
if (options.limit) {
@@ -69,11 +57,9 @@ export async function getTransactions(
6957
}
7058

7159
const queryString = params.toString();
72-
const endpoint = `/merchants/${merchantId}/payments${queryString ? `?${queryString}` : ""}`;
60+
const endpoint = `/v1/merchants/payments${queryString ? `?${queryString}` : ""}`;
7361

7462
return apiClient.get<TransactionsResponse>(endpoint, {
75-
headers: {
76-
"x-api-key": MERCHANT_PORTAL_API_KEY,
77-
},
63+
headers,
7864
});
7965
}

0 commit comments

Comments
 (0)