Skip to content

Commit

Permalink
Merge pull request #26 from Foxy/fix/sub_modify-and-sub_restart-at-it…
Browse files Browse the repository at this point in the history
…em-level

Fix/sub modify and sub restart at item level
  • Loading branch information
brettflorio authored Mar 22, 2021
2 parents a12472d + d42dce9 commit 570f08d
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 138 deletions.
2 changes: 1 addition & 1 deletion custom-elements.json
Original file line number Diff line number Diff line change
Expand Up @@ -2726,4 +2726,4 @@
]
}
]
}
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 17 additions & 48 deletions src/elements/public/ItemsForm/ItemsForm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ItemsForm } from './ItemsForm';
import { MockItem } from '../../../mocks/FxItem';
import { Dropdown, ErrorScreen } from '../../private/index';

const cartWideFields = ['sub_token', 'sub_modify', 'sub_restart', 'sub_cancel'];

/**
* Avoid CustomElementsRegistry collisions
*
Expand Down Expand Up @@ -428,13 +430,21 @@ describe('The form submits a valid POST to forxycart', async function () {
expect(form).to.exist;
const fd = new FormData(form);
for (const k of fd.keys()) {
if (k != 'cart') expect(k.match(/^\d+:.*$/)!.index).to.equal(0);
if (k != 'cart') {
if (!cartWideFields.includes(k)) {
expect(k.match(/^\d+:.*$/)!.index).to.equal(0);
}
}
}
});

it('Concatenates signatures', async function () {
it('Uses signed versions of field names', async function () {
const el = await fixture(html`
<test-items-form currency="usd" store="test.foxycart.com">
<test-items-form
currency="usd"
store="test.foxycart.com"
signatures="${JSON.stringify(signatures)}"
>
<x-testitem name="p1" code="MyCode" price="10.00" quantity="3"></x-testitem>
<x-testitem name="p2" code="MyCode2" price="10.00" quantity="1"></x-testitem>
</test-items-form>
Expand All @@ -456,50 +466,9 @@ describe('The form submits a valid POST to forxycart', async function () {
const fd = new FormData(form);
for (const [k, v] of fd.entries()) {
if (k != 'cart') {
expect(k).to.match(/.*\|\|a{64}$/);
}
}
});

it('Concatenates open to custom fields', async function () {
const el = await fixture(html`
<test-items-form currency="usd" store="test.foxycart.com">
<x-testitem
name="p1"
data-item
code="ITEMWITHOPENFIELD"
price="10.00"
quantity="3"
></x-testitem>
</test-items-form>
`);
const open = { color: true };
await elementUpdated(el);
const items = el.querySelectorAll('[data-item]');
expect(items).to.exist;
(signatures as any).color = signatures.name;
let last: MockItem | null = null;
for (const p of items) {
(p as MockItem).signatures = signatures;
(p as MockItem).open = open;
(p as MockItem).color = 'blue';
p.setAttribute('color', 'blue');
last = p as MockItem;
}
if (last) {
last.dispatchEvent(new CustomEvent('change'));
}
await elementUpdated(el);
const form = el.shadowRoot!.querySelector('form') as HTMLFormElement;
const fd = new FormData(form);
let found = false;
for (const k of fd.keys()) {
if (k.match(/\d+:color\|\|a{64}\|\|open$/)) {
found = true;
break;
expect(k).to.match(/.*signed.*/);
}
}
expect(found).to.equal(true);
});

it('Does not create empty frequency field', async function () {
Expand Down Expand Up @@ -533,7 +502,7 @@ describe('The form submits a valid POST to forxycart', async function () {
const form = el.shadowRoot!.querySelector('form') as HTMLFormElement;
let freqFound = false;
for (const e of new FormData(form).entries()) {
if (e[0].match(/^[0-9]+:sub_frequency$/)) {
if (e[0].match(/sub_frequency||signed/)) {
freqFound = true;
break;
}
Expand Down Expand Up @@ -581,11 +550,11 @@ describe('The form submits a valid POST to forxycart', async function () {
freqStartEnd[0] += 1;
expect(e[1]).to.equal('3m');
}
if (e[0].match(/.*sub_startdate$/)) {
if (e[0].match(/.*sub_startdate/)) {
freqStartEnd[1] += 1;
expect(e[1]).to.equal('30');
}
if (e[0].match(/.*sub_enddate$/)) {
if (e[0].match(/.*sub_enddate/)) {
freqStartEnd[2] += 1;
expect(e[1]).to.equal('22220101');
}
Expand Down
92 changes: 39 additions & 53 deletions src/elements/public/ItemsForm/ItemsForm.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import '@vaadin/vaadin-button';
import { html, PropertyDeclarations, TemplateResult } from 'lit-element';
import { Translatable } from '../../../mixins/translatable';
import { parseDuration } from '../../../utils/parse-duration';
import { Dropdown, ErrorScreen } from '../../private/index';
import { ItemsFormChangeEvent, ItemsFormSubmitEvent } from './events';
import { Item } from './private/Item';
import { ItemInterface } from './types';
import { SignableFields } from './private/SignableFields';

export { Item };

Expand All @@ -21,7 +21,7 @@ export { Item };
* @element foxy-items-form
*
*/
export class ItemsForm extends Translatable {
export class ItemsForm extends SignableFields {
/** @readonly */
public static get scopedElements(): Record<string, unknown> {
return {
Expand Down Expand Up @@ -484,6 +484,7 @@ export class ItemsForm extends Translatable {
this.__itemElements.forEach(e => {
added += this.__formDataAddItem(fd, e);
});
this.__formDataAddCartWideSubscriptionFields(fd);
return added;
}

Expand All @@ -505,49 +506,50 @@ export class ItemsForm extends Translatable {
});
}

/**
* Adds a item to a form data
*
* @argument {FormData} fd the FormData to which the item will be added
* @argument {Item} the item to add
**/
private __formDataAddItem(fd: FormData, itemEl: Item, parent: Item | null = null): number {
const idKey = 'pid';
// Reserved attributes are not to be submitted
// other attributes, included custom attributes added by the user, will be submitted
// Reserved attributes are not to be submitted
// other attributes, included custom attributes added by the user, will be submitted
private __isAttributeReserved(attribute: string): boolean {
const reservedAttributes = [
idKey,
'signatures',
'currency',
'total',
'slot',
'alt',
'currency',
'description',
'isChildren',
'isItem',
'open',
'items',
'open',
'pid',
'signatures',
'slot',
'total',
];
return reservedAttributes.includes(attribute);
}

/**
* Adds a item to a form data
*
* @argument {FormData} fd the FormData to which the item will be added
* @argument {Item} the item to add
**/
private __formDataAddItem(fd: FormData, itemEl: Item, parent: Item | null = null): number {
let added = 0;
if (this.__validItem(itemEl)) {
if (!itemEl.value[idKey]) {
if (!itemEl.value['pid']) {
throw new Error('Attempt to convert a item without a propper ID');
}
if (parent && parent.getAttribute('code')) {
itemEl.setAttribute('parent_code', parent.getAttribute('code')!);
}
for (let i = 0; i < itemEl.attributes.length; i++) {
const field = itemEl.attributes[i];
if (!reservedAttributes.includes(field.name) && !field.name.startsWith('data-')) {
if (!this.__isAttributeReserved(field.name) && !field.name.startsWith('data-')) {
let fieldValue: unknown = (itemEl as any)[field.name];
// Adds a signature if possible
if (itemEl.code && ['string', 'number'].includes(typeof fieldValue)) {
if (parent && field.name == 'quantity') {
fieldValue = Number(field.value) * parent.quantity;
}
const key = this.__buildKeyFromItem(field.name, itemEl);
fd.set(key, (fieldValue as string | number).toString());
fd.set(itemEl.signedName(field.name), (fieldValue as string | number).toString());
}
}
}
Expand All @@ -563,24 +565,6 @@ export class ItemsForm extends Translatable {
return added;
}

// build a key with prepended id and appended signature and |open given an item
private __buildKeyFromItem(key: string, item: Item) {
return this.__buildKey(
item.pid!.toString(),
key,
item.signatures ? (item.signatures[key] as string) : '',
!!item.open && item.open[key]
);
}

// builds a id prepended signature and |open appended key
private __buildKey(id: string, key: string, signature: string, open: boolean) {
if (signature) {
key = this.__addSignature(key, signature, open);
}
return `${id}:${key}`;
}

/**
* Adds subscription fields to a FormData
*
Expand All @@ -591,22 +575,24 @@ export class ItemsForm extends Translatable {
if (this.sub_frequency) {
for (const s of ['sub_frequency', 'sub_startdate', 'sub_enddate']) {
if ((this as any)[s]) {
const subKey = this.__buildKeyFromItem(s, itemEl);
fd.set(subKey, (this as any)[s]);
fd.set(itemEl.signedName(s), (this as any)[s]);
}
}
}
// added if themselves are set
for (const s of ['sub_token']) {
if ((this as any)[s]) {
const subKey = this.__buildKeyFromItem(s, itemEl);
fd.set(subKey, (this as any)[s]);
}
}
// added regardless
for (const s of ['sub_modify', 'sub_restart']) {
const subKey = this.__buildKeyFromItem(s, itemEl);
fd.set(subKey, (this as any)[s]);
}

/**
* Adds cart wide subscription fields to a FormData
*
* @argument {FormData} fd the FormData to which subscription fields will be added
*/
private __formDataAddCartWideSubscriptionFields(fd: FormData): void {
if (this.sub_frequency) {
// added if itself is set
if (this.sub_token) fd.set(this.signedName('sub_token'), this.sub_token);
// added regardless
fd.set(this.signedName('sub_modify'), this.sub_modify);
fd.set(this.signedName('sub_restart'), this.sub_restart);
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/elements/public/ItemsForm/private/Item.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,14 @@ describe('Item recognizes its children', async function () {
});
});

describe('Item builds its signed names', async () => {
it('Prepends ids to the field names', async function () {
const layout = html`<test-item name="p1" quantity="1"></test-item>`;
const element = await fixture<TestItem>(layout);
expect(element.signedName('quantity')).to.match(/^\d+:quantity.*/);
});
});

describe('Item displays price and total amount', async () => {
let logSpy: sinon.SinonStub;

Expand Down
49 changes: 14 additions & 35 deletions src/elements/public/ItemsForm/private/Item.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import '@vaadin/vaadin-text-field/vaadin-integer-field';
import { css, CSSResultArray, html, PropertyDeclarations, TemplateResult } from 'lit-element';
import { Translatable } from '../../../../mixins/translatable';
import { ErrorScreen, I18N } from '../../../private/index';
import { ImageDescription, ItemInterface } from '../types';
import { SignableFields } from './SignableFields';
import { Preview } from './Preview';

/**
Expand All @@ -13,7 +13,7 @@ import { Preview } from './Preview';
* @csspart picture - Image of the product in preview stack (for single products) or grid (for bundles).
* @csspart item - The root element inside of the shadow dom.
*/
export class Item extends Translatable {
export class Item extends SignableFields {
// A list of item properties as defined in Foxy Cart Documentation

/** @readonly */
Expand Down Expand Up @@ -118,20 +118,6 @@ export class Item extends Translatable {
open: { type: Object },
pid: { type: Number, reflect: true },
items: { type: Array },
signatures: {
type: Object,
converter: value => {
const v = (JSON.parse(value!) as unknown) as Record<string, string>;
for (const k of Object.keys(v)) {
if ((v[k] as string).length != 64) {
console.error(
'There is something wrong with the signature. It should have 64 characters.'
);
}
}
return v;
},
},
};
}

Expand Down Expand Up @@ -235,25 +221,6 @@ export class Item extends Translatable {
*/
public shipto?: string;

/**
* Optional open: An Object with key, value pairs where the key is a item
* attribute and the value is a previously computed HMAC validation code.
*
* **Important security information:** this web component does not generate or validates the HMAC validation code.
* Please, refer to [the Product Verification page](https://wiki.foxycart.com/v/2.0/hmac_validation) for more information and tools for generating the codes.
*/
public signatures?: Record<string, string>;

/**
* Optional open: An Object with key, value pairs where the key is a item
* attribute and the value is a boolean indicating that the property is editable by the user.
*
* **Advanced use only**: this web component does not provide means for the user to alter item attributes.
*
* See [Product Verification](https://wiki.foxycart.com/v/2.0/hmac_validation) for more information.
*/
public open?: Record<string, boolean>;

/**
* Optional length. This property affects cart UI only.
*/
Expand Down Expand Up @@ -500,6 +467,18 @@ export class Item extends Translatable {
};
}

/**
* Items have their signed names prefixed with their id.
*
* @argument fieldName the name of the field to get the signed version.
* @argument open whether this field is editable by the user.
* @returns signed version of the name, prefixed with the item id.
*/
public signedName(fieldName: string): string {
const signed = super.signedName(fieldName);
return `${this.pid.toString()}:${signed}`;
}

/**
* Creates a new unique id to be used in the form
*
Expand Down
Loading

0 comments on commit 570f08d

Please sign in to comment.