Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: pick & offer the single best virtual card to user #277

Merged
merged 50 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
4e757a6
chore: add reloadly cards & countries lists
EresDev Aug 15, 2024
a01ee92
feat: display single suitable virutal card to user
EresDev Aug 21, 2024
c281cbe
fix: pick intl mastercard/visa for allowed countries
EresDev Aug 22, 2024
aa300f1
fix: pick local mastercard/visa as last option
EresDev Aug 22, 2024
632b858
refactor: shorten vars' names
EresDev Aug 22, 2024
061bde2
fix: load ordered card details with order
EresDev Aug 23, 2024
35dc73b
fix: replace card list with single card in UI
EresDev Aug 23, 2024
97c7198
fix: update UI for single virtual card
EresDev Aug 26, 2024
b07f8fd
fix: place redeem info in new UI
EresDev Aug 26, 2024
e10871b
fix: update UI for purchased virtual card
EresDev Aug 27, 2024
6bae6f4
fix: improve ui
EresDev Aug 27, 2024
dd9cf0d
refactor: repalce term claim card with mint card
EresDev Aug 27, 2024
3bc8d5c
fix: apply changes for dark/light theme consistancy
EresDev Aug 27, 2024
11d6dd9
fix: stop minting card if permit is not claimable
EresDev Aug 27, 2024
619667d
fix: make sandbox usable with special function
EresDev Aug 28, 2024
c57c245
fix: check allowed country before loading cards
EresDev Aug 28, 2024
61e7b24
refactor: load cards list in selector function
EresDev Aug 28, 2024
e366bc2
fix: check suitable card on order placement
EresDev Aug 28, 2024
7994fa1
fix: send correct product id to post order
EresDev Aug 28, 2024
2589db9
refactor: reword claim to mint
EresDev Aug 28, 2024
56f62af
chore: fix spellings
EresDev Aug 28, 2024
f36277b
refactor: rename functions & routes
EresDev Aug 28, 2024
71e017a
refactor: move activate info with card html
EresDev Aug 28, 2024
af0b71f
refactor: improve html class names
EresDev Aug 28, 2024
85f7137
refactor: remove redundant activate action
EresDev Aug 28, 2024
4e6929b
refactor: remove redundant html element
EresDev Aug 28, 2024
4a122cf
fix: adjust ui shadow & opacity of light mode
EresDev Aug 28, 2024
715accf
feat: check for permit expiry
EresDev Aug 29, 2024
d6b76ea
docs: fix reason for sandbox function
EresDev Aug 29, 2024
3f09881
fix: improve minting success msg
EresDev Aug 29, 2024
793cf69
refactor: remove xml doctype for svg
EresDev Aug 29, 2024
1cc67af
refactor: lazy load virtual cards from API
EresDev Aug 29, 2024
161dec2
test: fix broken e2e tests
EresDev Aug 29, 2024
8390a86
fix: set card treasury address
EresDev Aug 29, 2024
ac5e590
test: update ip mock data
EresDev Aug 29, 2024
b566de8
fix: consider reward amount in picking best card
EresDev Aug 29, 2024
d1841ab
chore: change treasury for testing on mainnet
EresDev Aug 29, 2024
18f3ae6
Revert "chore: change treasury for testing on mainnet"
EresDev Aug 29, 2024
9a2fb76
feat: add wait mechanism for delayed minting
EresDev Aug 29, 2024
dbd5ffe
fix: apply correct border to card img
EresDev Aug 31, 2024
5b976d9
refactor: use record for country list
EresDev Sep 5, 2024
db710e4
docs: add info to use virtual card feature
EresDev Sep 6, 2024
255e221
docs: fix spellings
EresDev Sep 6, 2024
69dfbeb
fix: correct country name in allowed list
EresDev Sep 11, 2024
ed530bd
refactor: remove redundant space
EresDev Sep 11, 2024
d4083ca
fix: reword the error message
EresDev Sep 11, 2024
ed6accb
fix: reword heading
EresDev Sep 11, 2024
619193a
fix: reword error message
EresDev Sep 11, 2024
60e8a4d
fix: reword purchased card heading
EresDev Sep 11, 2024
2bb4c20
fix: reword minting error message
EresDev Sep 11, 2024
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
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"Rpcs",
"scalarmult",
"servedir",
"skus",
"solmate",
"sonarjs",
"SUPABASE",
Expand Down
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,35 @@ A vanilla Typescript dApp for claiming Ubiquity Rewards. It also includes tools
PAYMENT_TOKEN_ADDRESS="0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"

# Variables depending on spender (bounty hunter)
AMOUNT_IN_ETH="1"
AMOUNT_IN_ETH="50"
BENEFICIARY_ADDRESS="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
```

3. Update values for wrangler variables to use Reloadly sandbox or production API in the `wrangler.toml` file.

```
[vars]
USE_RELOADLY_SANDBOX = "true"
RELOADLY_API_CLIENT_ID = "xxxxxxxxxxxxxxxxxx"
RELOADLY_API_CLIENT_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

## Local Testing

1. Set `.env` variables.
2. Run `yarn test:anvil` in terminal A and `yarn test:fund` in terminal B.
3. In terminal B, run `yarn start`.
2. Run `yarn`
3. Run `yarn test:anvil` in terminal A and `yarn test:fund` in terminal B.
4. In terminal B, run

```
yarn build
yarn start
```

4. A permit URL for both ERC20 and ERC721 will be generated.
5. Open the generated permit URL from the console.
6. Connect your wallet (import anvil accounts [0] & [1] into your wallet).
7. Depending on your connected account, either the claim or invalidate button will be visible.
7. Depending on your connected account, either the claim or invalidate button will be visible. The virtual card section will also display an available virtual card.
8. To test ERC721 permits, deploy the `nft-rewards` contract from the [repository](https://github.com/ubiquity/nft-rewards).

### Importing Anvil Accounts
Expand Down Expand Up @@ -71,6 +87,19 @@ A vanilla Typescript dApp for claiming Ubiquity Rewards. It also includes tools
- Ensure `.env` is correctly configured and wallet provider network is correct if `Allowance` or `Balance` is `0.00`.
- Always start the Anvil instance before using `yarn start` as permit generation requires an on-chain call to `token.decimals()`.

### Troubleshooting virtual cards

Virtual cards are subject to regulations and are not available for all countries. Moreover, each virtual card is available for specific amounts. If you are unable to see an available virtual card it is either because of your location or the amount of your permit.

If you are not getting an available card, you can perform a few extra steps to get a virtual card for testing purposes. You can set the permit amount `AMOUNT_IN_ETH` to be 50 WXDAI in the `.env` file and mock your location as United States. To set your location to United States, you can follow one of the steps given below:

- Use a USA VPN
- Set your timezone to `Eastern Time (ET) New York` and block the ajax request to `https://ipinfo.io/json` so that your timezone is used to detect your location.

One of these steps should get you a virtual card to try both on Reloadly sandbox and production. Please note that if you are minting a virtual card with a mock location on Reloadly production, you will get a redeem code but you may not able to use the card due to restrictions on the card, and there is no refund or replacement. Use your real location if you want to use the virtual card.

If you are using mainnet with your local environments, you may want to change the `giftCardTreasuryAddress` to a wallet that you own in the file `shared/constants.ts`. It is the wallet where payments for the virtual cards are sent.

## How to generate a permit2 URL using the script

1. Admin sets `env.AMOUNT_IN_ETH` and `env.BENEFICIARY_ADDRESS` depending on a bounty hunter's reward and address
Expand Down
98 changes: 51 additions & 47 deletions cypress/e2e/claim-gift-card.cy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { JsonRpcProvider, JsonRpcSigner } from "@ethersproject/providers";
import { Wallet } from "ethers";
import { PermitConfig, generateERC20Permit } from "../../scripts/typescript/generate-erc20-permit-url";
import { PermitConfig, generateErc20Permit } from "../../scripts/typescript/generate-erc20-permit-url";

const beneficiary = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; // anvil
const SENDER_PRIVATE_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; // anvil
Expand All @@ -16,25 +16,18 @@ describe("Gift Cards", () => {
setupIntercepts();
});

it("should show/hide activation info", () => {
it.only("should show redeem info", () => {
const permitConfig = Cypress.env("permitConfig");
void cy.getPermitUrl(permitConfig).then((permitUrl) => {
cy.visit(`${permitUrl as string}`);
cy.wait(2000);

cy.wait("@listGiftCards");

cy.get("#gift-cards").should("exist").and("include.text", "Or claim in virtual visa/mastercard");
cy.get(".gift-card").should("have.length.above", 0);

cy.get(".gift-card.purchased").should("not.exist");

cy.get('#activate-info .redeem-info-wrapper[data-show="true"]').should("not.exist");
cy.get(".gift-card").eq(0).find(".activate-btn").invoke("click");
cy.wait("@getBestCard");
cy.wait(2000);

cy.get('#activate-info .redeem-info-wrapper[data-show="true"]').should("exist");
cy.get("#activate-info .close-btn").invoke("click");
cy.get('#activate-info .redeem-info-wrapper[data-show="true"]').should("not.exist");
cy.get("#gift-cards").should("exist").and("include.text", "Or mint a virtual visa/mastercard");
cy.get(".card-section").should("have.length.above", 0);
cy.get(".redeem-info").should("exist");
cy.get(".redeem-info").eq(0).should("include.text", "How to use redeem code?");
});
});

Expand All @@ -47,34 +40,30 @@ describe("Gift Cards", () => {
cy.visit(permitUrl);
cy.wait(2000);

cy.wait("@listGiftCards");
cy.get(".gift-card").should("have.length.above", 0);
cy.get(".gift-card .available").should("have.length.above", 0);
cy.get(".gift-card .available")
.eq(0)
.parent()
.parent()
.find("h3")
.eq(0)
.then(($name) => {
const giftCardName = $name;
cy.wrap(giftCardName).as("giftCardName");
});

cy.intercept({ method: "POST", url: "/post-order" }).as("postOrder");
cy.wait("@getBestCard");
cy.get(".card-section").should("have.length.above", 0);
cy.get("#offered-card").should("exist");
cy.get("#offered-card .details h3").then(($name) => {
const giftCardName = $name;
cy.wrap(giftCardName).as("giftCardName");
});

cy.get(".gift-card .available").eq(0).parent().parent().find(".claim-gift-card-btn").should("have.length", 1);
cy.intercept({ method: "POST", url: "/post-order?country=US" }).as("postOrder");

cy.get("#offered-card .details #mint").should("exist");
cy.intercept({ method: "GET", url: "/get-order**" }).as("getOrder");
cy.get(".gift-card .available").eq(0).parent().parent().find(".claim-gift-card-btn").invoke("click");

cy.get("#offered-card .details #mint").invoke("click");

cy.get(".notifications", { timeout: 10000 }).should("contain.text", "Processing... Please wait. Do not close this page.");
cy.get(".notifications", { timeout: 10000 }).should("contain.text", "Transaction confirmed. Loading your card now.");
cy.get(".notifications", { timeout: 10000 }).should("contain.text", "Transaction confirmed. Minting your card now.");
cy.wait("@getOrder", { timeout: 10000 });

cy.get("#gift-cards").should("exist").and("include.text", "Your gift card");
cy.get("#gift-cards").should("exist").and("include.text", "Your virtual visa/mastercard");

cy.get("#redeem-code").should("exist");
cy.get("@giftCardName").then((name) => {
cy.get(".gift-card h3")
cy.get("#offered-card .details h3")
.eq(0)
.should("have.text", name.text() as string);
});
Expand All @@ -87,18 +76,18 @@ describe("Gift Cards", () => {
);
cy.wait(2000);

cy.wait("@listGiftCards");
cy.wait("@getBestCard");

cy.get("#gift-cards").should("exist").and("include.text", "Your gift card");
cy.get(".gift-card.redeem-code > h3").eq(0).should("have.text", "Your redeem code");
cy.get(".gift-card.redeem-code > p").eq(0).should("have.text", "xxxxxxxxxxxx");
cy.get(".gift-card.redeem-code > p").eq(1).should("have.text", "xxxxxxxxxxxx");
cy.get(".gift-card.redeem-code > p").eq(2).should("have.text", "xxxxxxxxxxxx");
cy.get(".gift-card.redeem-code > .buttons > #reveal-btn").invoke("click");
cy.get("#gift-cards").should("exist").and("include.text", "Your virtual visa/mastercard");
cy.get("#redeem-code > h3").eq(0).should("have.text", "Redeem code");
cy.get("#redeem-code > p").eq(0).should("have.text", "xxxxxxxxxxxx");
cy.get("#redeem-code > p").eq(1).should("have.text", "xxxxxxxxxxxx");
cy.get("#redeem-code > p").eq(2).should("have.text", "xxxxxxxxxxxx");
cy.get("#redeem-code > #reveal").invoke("click");

cy.get(".gift-card.redeem-code > h3").eq(0).should("have.text", "Your redeem code");
cy.get(".gift-card.redeem-code > p").should("exist");
cy.get(".gift-card.redeem-code > p").eq(0).should("not.have.text", "xxxxxxxxxxxx");
cy.get("#redeem-code > h3").eq(0).should("have.text", "Redeem code");
cy.get("#redeem-code > p").should("exist");
cy.get("#redeem-code > p").eq(0).should("not.have.text", "xxxxxxxxxxxx");
});
});

Expand Down Expand Up @@ -139,7 +128,22 @@ function setupIntercepts() {
body: {},
});

cy.intercept({ method: "GET", url: "/list-gift-cards" }).as("listGiftCards");
cy.intercept({ method: "GET", url: "/get-best-card?country=US**" }).as("getBestCard");
cy.intercept("GET", "https://ipinfo.io/json", {
statusCode: 200,
body: {
ip: "192.158.1.38",
hostname: "example.com",
city: "Los Angeles",
region: "California",
country: "US",
loc: "34.0522,-118.2437",
org: "Example org",
postal: "90009",
timezone: "America/Los_Angeles",
readme: "https://ipinfo.io/missingauth",
},
});
}

function stubEthereum(signer: JsonRpcSigner) {
Expand Down Expand Up @@ -192,5 +196,5 @@ function providerFunctions(method: string) {
}

Cypress.Commands.add("getPermitUrl", (customPermitConfig: PermitConfig) => {
return generateERC20Permit(customPermitConfig);
return generateErc20Permit(customPermitConfig);
});
1 change: 0 additions & 1 deletion cypress/e2e/main.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@ describe("Homepage tests", () => {
});
cy.get("@consoleError").should("not.be.called");
cy.get("body").should("exist");
cy.get("h1").should("exist");
});
});
30 changes: 30 additions & 0 deletions functions/get-best-card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { BigNumber } from "ethers";
import { getAccessToken, findBestCard } from "./helpers";
import { Context } from "./types";
import { validateEnvVars, validateRequestMethod } from "./validators";

export async function onRequest(ctx: Context): Promise<Response> {
try {
validateRequestMethod(ctx.request.method, "GET");
validateEnvVars(ctx);

const { searchParams } = new URL(ctx.request.url);
const country = searchParams.get("country");
const amount = searchParams.get("amount");

if (isNaN(Number(amount)) || !(country && amount)) {
throw new Error(`Invalid query parameters: ${{ country, amount }}`);
}

const accessToken = await getAccessToken(ctx.env);
const bestCard = await findBestCard(country, BigNumber.from(amount), accessToken);

if (bestCard) {
return Response.json(bestCard, { status: 200 });
}
return Response.json({ message: "There are no gift cards available." }, { status: 404 });
} catch (error) {
console.error("There was an error while processing your request.", error);
return Response.json({ message: "There was an error while processing your request." }, { status: 500 });
}
}
8 changes: 7 additions & 1 deletion functions/get-order.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OrderTransaction } from "../shared/types";
import { commonHeaders, getAccessToken, getBaseUrl } from "./helpers";
import { getGiftCardById } from "./post-order";
import { AccessToken, Context, ReloadlyFailureResponse, ReloadlyGetTransactionResponse } from "./types";
import { validateEnvVars, validateRequestMethod } from "./validators";

Expand All @@ -22,7 +23,12 @@ export async function onRequest(ctx: Context): Promise<Response> {
if (!reloadlyTransaction) {
return Response.json("Order not found.", { status: 404 });
} else if (reloadlyTransaction.status && reloadlyTransaction.status == "SUCCESSFUL") {
return Response.json(reloadlyTransaction, { status: 200 });
try {
const product = await getGiftCardById(reloadlyTransaction.product.productId, accessToken);
return Response.json({ transaction: reloadlyTransaction, product: product }, { status: 200 });
} catch (error) {
return Response.json({ transaction: reloadlyTransaction, product: null }, { status: 200 });
EresDev marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
return Response.json({ message: "There is no successful transaction for given order ID." }, { status: 404 });
}
Expand Down
Loading
Loading