Skip to content

Commit 7d4f34c

Browse files
feat(pos-app): add default merchant credentials from environment variables (#324)
* feat(pos-app): add default merchant credentials from environment variables - Add EXPO_PUBLIC_DEFAULT_MERCHANT_ID and EXPO_PUBLIC_DEFAULT_MERCHANT_API_KEY env vars - Initialize merchant credentials from env defaults on first app launch - Allow users to override defaults via settings (with PIN protection) - Reset both merchant ID and API key to defaults when clearing merchant ID - Sync UI state after resetting to show new default values Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(pos-app): return new merchant ID from clearMerchantId Make the flow more explicit by returning the default merchant ID directly from clearMerchantId instead of reading from store afterward. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(pos-app): use nullish coalescing for env var defaults Use ?? instead of || to preserve empty string values from environment variables instead of converting them to null. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent aa7da67 commit 7d4f34c

File tree

6 files changed

+86
-27
lines changed

6 files changed

+86
-27
lines changed

dapps/pos-app/.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ EXPO_PUBLIC_PROJECT_ID=""
22
EXPO_PUBLIC_SENTRY_DSN=""
33
SENTRY_AUTH_TOKEN=""
44
EXPO_PUBLIC_API_URL=""
5-
EXPO_PUBLIC_GATEWAY_URL=""
5+
EXPO_PUBLIC_GATEWAY_URL=""
6+
EXPO_PUBLIC_DEFAULT_MERCHANT_ID=""
7+
EXPO_PUBLIC_DEFAULT_MERCHANT_API_KEY=""

dapps/pos-app/hooks/use-merchant-flow.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const initialState: MerchantFlowState = {
2929
export function useMerchantFlow() {
3030
const storedMerchantId = useSettingsStore((state) => state.merchantId);
3131
const setMerchantId = useSettingsStore((state) => state.setMerchantId);
32+
const clearMerchantId = useSettingsStore((state) => state.clearMerchantId);
3233
const getMerchantApiKey = useSettingsStore(
3334
(state) => state.getMerchantApiKey,
3435
);
@@ -123,16 +124,14 @@ export function useMerchantFlow() {
123124

124125
const handleMerchantIdConfirm = useCallback(async () => {
125126
const trimmedMerchantId = state.merchantIdInput.trim();
126-
if (!trimmedMerchantId) {
127-
return;
128-
}
129127

130128
// Check if value changed
131129
if (trimmedMerchantId === storedMerchantId) {
132130
return;
133131
}
134132

135-
await initiateSave(trimmedMerchantId, "merchant-id");
133+
// Pass empty string to indicate clearing (will reset to default)
134+
await initiateSave(trimmedMerchantId || "", "merchant-id");
136135
}, [state.merchantIdInput, storedMerchantId, initiateSave]);
137136

138137
const handleMerchantApiKeyConfirm = useCallback(async () => {
@@ -145,20 +144,37 @@ export function useMerchantFlow() {
145144
}, [state.merchantApiKeyInput, initiateSave]);
146145

147146
const completeSave = useCallback(async () => {
148-
if (!state.pendingValue || !state.pendingAction) {
147+
if (state.pendingValue === null || !state.pendingAction) {
149148
return;
150149
}
151150

152151
try {
153152
if (state.pendingAction === "merchant-id") {
154-
setMerchantId(state.pendingValue);
155-
showSuccessToast("Merchant ID saved successfully");
156-
addLog(
157-
"info",
158-
`Merchant ID updated to: ${state.pendingValue}`,
159-
"settings",
160-
"completeSave",
161-
);
153+
if (state.pendingValue === "") {
154+
// Clear merchant ID and API key (resets both to env defaults)
155+
const newMerchantId = await clearMerchantId();
156+
// Sync local input with the new default value
157+
setState((prev) => ({
158+
...prev,
159+
merchantIdInput: newMerchantId ?? "",
160+
}));
161+
showSuccessToast("Merchant credentials reset to default");
162+
addLog(
163+
"info",
164+
"Merchant credentials reset to default",
165+
"settings",
166+
"completeSave",
167+
);
168+
} else {
169+
setMerchantId(state.pendingValue);
170+
showSuccessToast("Merchant ID saved successfully");
171+
addLog(
172+
"info",
173+
`Merchant ID updated to: ${state.pendingValue}`,
174+
"settings",
175+
"completeSave",
176+
);
177+
}
162178
} else if (state.pendingAction === "merchant-api-key") {
163179
await setMerchantApiKey(state.pendingValue);
164180
setState((prev) => ({
@@ -186,6 +202,7 @@ export function useMerchantFlow() {
186202
state.pendingValue,
187203
state.pendingAction,
188204
setMerchantId,
205+
clearMerchantId,
189206
setMerchantApiKey,
190207
addLog,
191208
]);
@@ -257,9 +274,9 @@ export function useMerchantFlow() {
257274
}));
258275
}, [storedMerchantId]);
259276

277+
// Enable save when value has changed (including clearing to reset to default)
260278
const isMerchantIdConfirmDisabled =
261-
state.merchantIdInput.trim().length === 0 ||
262-
state.merchantIdInput.trim() === storedMerchantId;
279+
state.merchantIdInput.trim() === (storedMerchantId ?? "");
263280

264281
const isMerchantApiKeyConfirmDisabled =
265282
state.merchantApiKeyInput.trim().length === 0;

dapps/pos-app/services/payment.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useSettingsStore } from "@/store/useSettingsStore";
2-
import { SECURE_STORAGE_KEYS, secureStorage } from "@/utils/secure-storage";
32
import {
43
PaymentStatusResponse,
54
StartPaymentRequest,
@@ -14,9 +13,7 @@ import { apiClient } from "./client";
1413
*/
1514
async function getApiHeaders(): Promise<Record<string, string>> {
1615
const merchantId = useSettingsStore.getState().merchantId;
17-
const merchantApiKey = await secureStorage.getItem(
18-
SECURE_STORAGE_KEYS.MERCHANT_API_KEY,
19-
);
16+
const merchantApiKey = await useSettingsStore.getState().getMerchantApiKey();
2017

2118
if (!merchantId || merchantId.trim().length === 0) {
2219
throw new Error("Merchant ID is not configured");

dapps/pos-app/services/payment.web.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useSettingsStore } from "@/store/useSettingsStore";
2-
import { SECURE_STORAGE_KEYS, secureStorage } from "@/utils/secure-storage";
32
import {
43
ApiError,
54
PaymentStatusResponse,
@@ -17,9 +16,7 @@ async function getMerchantCredentials(): Promise<{
1716
apiKey: string;
1817
}> {
1918
const merchantId = useSettingsStore.getState().merchantId;
20-
const apiKey = await secureStorage.getItem(
21-
SECURE_STORAGE_KEYS.MERCHANT_API_KEY,
22-
);
19+
const apiKey = await useSettingsStore.getState().getMerchantApiKey();
2320

2421
if (!merchantId || merchantId.trim().length === 0) {
2522
throw new Error("Merchant ID is not configured");

dapps/pos-app/store/useSettingsStore.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { DEFAULT_LOGO_BASE64 } from "@/constants/printer-logos";
22
import { VariantName, Variants } from "@/constants/variants";
3+
import { MerchantConfig } from "@/utils/merchant-config";
34
import { SECURE_STORAGE_KEYS, secureStorage } from "@/utils/secure-storage";
45
import { storage } from "@/utils/storage";
56
import * as Crypto from "expo-crypto";
@@ -60,7 +61,7 @@ interface SettingsStore {
6061
setHasHydrated: (state: boolean) => void;
6162
setVariant: (variant: VariantName) => void;
6263
setMerchantId: (merchantId: string | null) => void;
63-
clearMerchantId: () => void;
64+
clearMerchantId: () => Promise<string | null>;
6465
setMerchantApiKey: (apiKey: string | null) => Promise<void>;
6566
clearMerchantApiKey: () => Promise<void>;
6667
getMerchantApiKey: () => Promise<string | null>;
@@ -100,8 +101,31 @@ export const useSettingsStore = create<SettingsStore>()(
100101
set({ themeMode: variantData.defaultTheme });
101102
}
102103
},
103-
setMerchantId: (merchantId: string | null) => set({ merchantId }),
104-
clearMerchantId: () => set({ merchantId: null }),
104+
setMerchantId: (merchantId: string | null) => {
105+
// If clearing, reset to env default
106+
if (!merchantId || merchantId.trim() === "") {
107+
set({ merchantId: MerchantConfig.getDefaultMerchantId() });
108+
} else {
109+
set({ merchantId });
110+
}
111+
},
112+
clearMerchantId: async () => {
113+
// Reset both merchant ID and API key to env defaults
114+
const defaultMerchantId = MerchantConfig.getDefaultMerchantId();
115+
set({ merchantId: defaultMerchantId });
116+
const defaultApiKey = MerchantConfig.getDefaultMerchantApiKey();
117+
if (defaultApiKey) {
118+
await secureStorage.setItem(
119+
SECURE_STORAGE_KEYS.MERCHANT_API_KEY,
120+
defaultApiKey,
121+
);
122+
set({ isMerchantApiKeySet: true });
123+
} else {
124+
await secureStorage.removeItem(SECURE_STORAGE_KEYS.MERCHANT_API_KEY);
125+
set({ isMerchantApiKeySet: false });
126+
}
127+
return defaultMerchantId;
128+
},
105129
setMerchantApiKey: async (apiKey: string | null) => {
106130
try {
107131
if (apiKey) {
@@ -268,6 +292,18 @@ export const useSettingsStore = create<SettingsStore>()(
268292
// Clean up migration data
269293
delete (state as any).__migrationData;
270294
}
295+
296+
// Initialize merchant defaults from env if not set
297+
const defaultMerchantId = MerchantConfig.getDefaultMerchantId();
298+
const defaultApiKey = MerchantConfig.getDefaultMerchantApiKey();
299+
300+
if (!state.merchantId && defaultMerchantId) {
301+
state.setMerchantId(defaultMerchantId);
302+
}
303+
304+
if (!state.isMerchantApiKeySet && defaultApiKey) {
305+
await state.setMerchantApiKey(defaultApiKey);
306+
}
271307
}
272308

273309
state?.setHasHydrated(true);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const DEFAULT_MERCHANT_ID = process.env.EXPO_PUBLIC_DEFAULT_MERCHANT_ID ?? null;
2+
const DEFAULT_MERCHANT_API_KEY =
3+
process.env.EXPO_PUBLIC_DEFAULT_MERCHANT_API_KEY ?? null;
4+
5+
export const MerchantConfig = {
6+
getDefaultMerchantId: (): string | null => DEFAULT_MERCHANT_ID,
7+
getDefaultMerchantApiKey: (): string | null => DEFAULT_MERCHANT_API_KEY,
8+
hasEnvDefaults: (): boolean =>
9+
Boolean(DEFAULT_MERCHANT_ID && DEFAULT_MERCHANT_API_KEY),
10+
};

0 commit comments

Comments
 (0)