Skip to content
Open
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
19 changes: 19 additions & 0 deletions src/actions/__tests__/payment-profile-actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,25 @@ describe("savePaymentProfile", () => {
expect(putRequest.mock.calls[0][3]).toEqual(EXISTING_PROFILE);
});

test("sends secret keys only when typed and never puts them in the request action", async () => {
store.dispatch(
savePaymentProfile({
...EXISTING_PROFILE,
has_live_secret_key: true,
has_test_secret_key: false,
live_secret_key: "",
test_secret_key: "sk_test_new"
})
);
await flushPromises();

expect(putRequest.mock.calls[0][3]).toEqual({
...EXISTING_PROFILE,
test_secret_key: "sk_test_new"
});
expect(JSON.stringify(store.getActions())).not.toContain("sk_test_new");
});

test("STOP_LOADING is dispatched even when the request fails", async () => {
putRequest.mockImplementation(
() => () => () => Promise.reject(new Error("network error"))
Expand Down
26 changes: 22 additions & 4 deletions src/actions/ticket-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,23 @@ export const getPaymentProfiles =
});
};

// secret keys are write-only: send them only when the user typed a new one,
// an omitted key keeps the stored value
const normalizePaymentProfile = (entity) => {
const normalizedEntity = { ...entity };

if (!normalizedEntity.live_secret_key)
delete normalizedEntity.live_secret_key;

if (!normalizedEntity.test_secret_key)
delete normalizedEntity.test_secret_key;

delete normalizedEntity.has_live_secret_key;
delete normalizedEntity.has_test_secret_key;

return normalizedEntity;
};

export const savePaymentProfile = (entity) => async (dispatch, getState) => {
const { currentSummitState } = getState();
const accessToken = await getAccessTokenSafely();
Expand All @@ -1269,14 +1286,15 @@ export const savePaymentProfile = (entity) => async (dispatch, getState) => {
access_token: accessToken
};

const normalizedEntity = normalizePaymentProfile(entity);

if (entity.id) {
return putRequest(
createAction(UPDATE_PAYMENT_PROFILE),
createAction(PAYMENT_PROFILE_UPDATED),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${entity.id}`,
entity,
snackbarErrorHandler,
entity
normalizedEntity,
snackbarErrorHandler
)(params)(dispatch)
.then(() => {
dispatch(
Expand All @@ -1293,7 +1311,7 @@ export const savePaymentProfile = (entity) => async (dispatch, getState) => {
createAction(UPDATE_PAYMENT_PROFILE),
createAction(PAYMENT_PROFILE_ADDED),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles`,
entity,
normalizedEntity,
snackbarErrorHandler
)(params)(dispatch)
.then(() => {
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2057,6 +2057,9 @@
"live_publishable_key": "Live Publishable Key",
"test_secret_key": "Test Secret Key",
"test_publishable_key": "Test Publishable Key",
"secret_key_set": "Secret key: set",
"secret_key_not_set": "Secret key: not set",
"replace": "Replace",
"info_stripe_keys": "Get your API keys from your stripe account.",
"info_stripe_test_mode": "Place the payment gateway in test mode using test API keys.",
"info_send_email_receipt": "Email Customer about successful payments.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ const feeTypeSectionEntity = {
test_mode_enabled: false,
send_email_receipt: false,
merchant_account_id: "",
live_secret_key: "sk_live_xxx",
has_live_secret_key: true,
live_publishable_key: "pk_live_xxx",
test_secret_key: "",
has_test_secret_key: false,
test_publishable_key: ""
};

Expand Down Expand Up @@ -267,6 +267,43 @@ describe("PaymentProfileDialog", () => {
});
});

test("a stored secret key shows as set, is only replaced through an empty password field and cancelling discards the typed value", async () => {
renderDialog({ entity: feeTypeSectionEntity });

expect(
screen.getByText("edit_payment_profile.secret_key_set")
).toBeInTheDocument();
expect(
screen.queryByTestId("input-live_secret_key")
).not.toBeInTheDocument();
// test key is not set, so its input is shown right away
expect(screen.getByTestId("input-test_secret_key")).toHaveAttribute(
"type",
"password"
);

fireEvent.click(
screen.getByRole("button", { name: "edit_payment_profile.replace" })
);
const liveInput = screen.getByTestId("input-live_secret_key");
expect(liveInput).toHaveAttribute("type", "password");
expect(liveInput).toHaveValue("");

fireEvent.change(liveInput, { target: { value: "sk_live_new" } });
fireEvent.click(screen.getByRole("button", { name: "general.cancel" }));
expect(
screen.queryByTestId("input-live_secret_key")
).not.toBeInTheDocument();

submitProfileForm();

await waitFor(() => {
expect(defaultProps.onSave).toHaveBeenCalledWith(
expect.objectContaining({ live_secret_key: "" })
);
});
});

test("provider selection shows the correct provider-specific fields", () => {
renderDialog({ entity: {} });

Expand Down
20 changes: 20 additions & 0 deletions src/pages/tickets/payment-profile/components/info-tooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";
import PropTypes from "prop-types";
import { Tooltip } from "@mui/material";
import InfoIcon from "@mui/icons-material/Info";

const InfoTooltip = ({ title }) => (
<Tooltip
title={title}
placement="top"
slotProps={{ tooltip: { sx: { fontSize: "0.625rem" } } }}
>
<InfoIcon fontSize="small" sx={{ ml: 0.5, verticalAlign: "middle" }} />
</Tooltip>
);

InfoTooltip.propTypes = {
title: PropTypes.string.isRequired
};

export default InfoTooltip;
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ import {
MenuItem,
Box,
InputLabel,
TextField,
Tooltip
TextField
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import AddIcon from "@mui/icons-material/Add";
import InfoIcon from "@mui/icons-material/Info";
import { useFormik, FormikProvider } from "formik";
import * as yup from "yup";
import MuiTable from "openstack-uicore-foundation/lib/components/mui/table";
Expand All @@ -31,20 +29,8 @@ import { currencyAmountFromCents } from "openstack-uicore-foundation/lib/utils/m
import useScrollToError from "../../../../hooks/useScrollToError";
import { nullableDecimalValidation } from "../../../../utils/yup";
import { ONE_HUNDRED, TEN_THOUSAND } from "../../../../utils/constants";

const InfoTooltip = ({ title }) => (
<Tooltip
title={title}
placement="top"
componentsProps={{ tooltip: { sx: { fontSize: "0.625rem" } } }}
>
<InfoIcon fontSize="small" sx={{ ml: 0.5, verticalAlign: "middle" }} />
</Tooltip>
);

InfoTooltip.propTypes = {
title: PropTypes.string.isRequired
};
import InfoTooltip from "./info-tooltip";
import SecretKeyField from "./secret-key-field";

const APPLICATION_TYPE_OPTIONS = [
{ label: "Registration", value: "Registration" },
Expand Down Expand Up @@ -115,9 +101,9 @@ const PaymentProfileDialog = ({
test_mode_enabled: initialEntity?.test_mode_enabled || false,
send_email_receipt: initialEntity?.send_email_receipt || false,
merchant_account_id: initialEntity?.merchant_account_id || "",
live_secret_key: initialEntity?.live_secret_key || "",
live_secret_key: "",
live_publishable_key: initialEntity?.live_publishable_key || "",
test_secret_key: initialEntity?.test_secret_key || "",
test_secret_key: "",
test_publishable_key: initialEntity?.test_publishable_key || ""
},
validationSchema: yup.object().shape({
Expand Down Expand Up @@ -283,7 +269,7 @@ const PaymentProfileDialog = ({
<FormikProvider value={formik}>
<DialogContent>
<Grid2 container spacing={2} mb={2}>
<Grid2 size={6}>
<Grid2 size={{ xs: 12, sm: 6 }}>
<InputLabel htmlFor="application_type">
{T.translate("edit_payment_profile.application_type")}
</InputLabel>
Expand All @@ -301,7 +287,7 @@ const PaymentProfileDialog = ({
))}
</MuiFormikSelect>
</Grid2>
<Grid2 size={6}>
<Grid2 size={{ xs: 12, sm: 6 }}>
<InputLabel htmlFor="provider">
{T.translate("edit_payment_profile.provider")}
</InputLabel>
Expand All @@ -314,13 +300,13 @@ const PaymentProfileDialog = ({
</MuiFormikSelect>
</Grid2>

<Grid2 size={4}>
<Grid2 size={{ xs: 12, sm: 4 }}>
<MuiFormikCheckbox
name="is_active"
label={T.translate("edit_payment_profile.active")}
/>
</Grid2>
<Grid2 size={4} sx={{ display: "flex" }}>
<Grid2 size={{ xs: 12, sm: 4 }} sx={{ display: "flex" }}>
<MuiFormikCheckbox
name="test_mode_enabled"
label={
Expand All @@ -336,7 +322,7 @@ const PaymentProfileDialog = ({
/>
</Grid2>
{formik.values.provider === "Stripe" && (
<Grid2 size={4}>
<Grid2 size={{ xs: 12, sm: 4 }}>
<MuiFormikCheckbox
name="send_email_receipt"
label={
Expand Down Expand Up @@ -366,21 +352,15 @@ const PaymentProfileDialog = ({
</Grid2>
)}

<Grid2 size={6}>
<InputLabel htmlFor="live_secret_key">
{T.translate("edit_payment_profile.live_secret_key")}
<InfoTooltip
title={T.translate("edit_payment_profile.info_stripe_keys")}
/>
</InputLabel>
<MuiFormikTextField
<Grid2 size={{ xs: 12, sm: 6 }}>
<SecretKeyField
name="live_secret_key"
required
fullWidth
variant="outlined"
label={T.translate("edit_payment_profile.live_secret_key")}
isSet={Boolean(initialEntity.has_live_secret_key)}
required={!initialEntity.has_live_secret_key}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
/>
</Grid2>
<Grid2 size={6}>
<Grid2 size={{ xs: 12, sm: 6 }}>
<InputLabel htmlFor="live_publishable_key">
{T.translate("edit_payment_profile.live_publishable_key")}
<InfoTooltip
Expand All @@ -394,20 +374,14 @@ const PaymentProfileDialog = ({
variant="outlined"
/>
</Grid2>
<Grid2 size={6}>
<InputLabel htmlFor="test_secret_key">
{T.translate("edit_payment_profile.test_secret_key")}
<InfoTooltip
title={T.translate("edit_payment_profile.info_stripe_keys")}
/>
</InputLabel>
<MuiFormikTextField
<Grid2 size={{ xs: 12, sm: 6 }}>
<SecretKeyField
name="test_secret_key"
fullWidth
variant="outlined"
label={T.translate("edit_payment_profile.test_secret_key")}
isSet={Boolean(initialEntity.has_test_secret_key)}
/>
</Grid2>
<Grid2 size={6}>
<Grid2 size={{ xs: 12, sm: 6 }}>
<InputLabel htmlFor="test_publishable_key">
{T.translate("edit_payment_profile.test_publishable_key")}
<InfoTooltip
Expand Down Expand Up @@ -459,7 +433,7 @@ const PaymentProfileDialog = ({
<FormikProvider value={feeTypeFormik}>
{showFeeTypeForm && (
<Grid2 size={12} container spacing={2} mb={2}>
<Grid2 size={4}>
<Grid2 size={{ xs: 12, sm: 4 }}>
<InputLabel htmlFor="name">
{T.translate(
"edit_payment_profile.payment_type_fee_name"
Expand All @@ -472,7 +446,7 @@ const PaymentProfileDialog = ({
margin="none"
/>
</Grid2>
<Grid2 size={4}>
<Grid2 size={{ xs: 12, sm: 4 }}>
<InputLabel htmlFor="kind">
{T.translate(
"edit_payment_profile.payment_type_fee_kind"
Expand Down Expand Up @@ -506,7 +480,7 @@ const PaymentProfileDialog = ({
))}
</MuiFormikSelect>
</Grid2>
<Grid2 size={4}>
<Grid2 size={{ xs: 12, sm: 4 }}>
<InputLabel htmlFor="payment_method">
{T.translate(
"edit_payment_profile.payment_type_fee_method"
Expand Down Expand Up @@ -539,7 +513,7 @@ const PaymentProfileDialog = ({
))}
</MuiFormikSelect>
</Grid2>
<Grid2 size={4}>
<Grid2 size={{ xs: 12, sm: 4 }}>
<InputLabel htmlFor="value">
{T.translate(
"edit_payment_profile.payment_type_fee_value"
Expand Down Expand Up @@ -597,7 +571,7 @@ const PaymentProfileDialog = ({
/>
)}
</Grid2>
<Grid2 size={4}>
<Grid2 size={{ xs: 12, sm: 4 }}>
<InputLabel htmlFor="max_cents">
{T.translate(
"edit_payment_profile.payment_type_fee_max_cents"
Expand All @@ -611,7 +585,7 @@ const PaymentProfileDialog = ({
inCents
/>
</Grid2>
<Grid2 size={4}>
<Grid2 size={{ xs: 12, sm: 4 }}>
<InputLabel htmlFor="min_cents">
{T.translate(
"edit_payment_profile.payment_type_fee_min_cents"
Expand Down Expand Up @@ -700,9 +674,9 @@ PaymentProfileDialog.propTypes = {
test_mode_enabled: PropTypes.bool,
send_email_receipt: PropTypes.bool,
merchant_account_id: PropTypes.string,
live_secret_key: PropTypes.string,
has_live_secret_key: PropTypes.bool,
live_publishable_key: PropTypes.string,
test_secret_key: PropTypes.string,
has_test_secret_key: PropTypes.bool,
test_publishable_key: PropTypes.string
}),
paymentFeeTypes: PropTypes.shape({
Expand Down
Loading
Loading