Skip to content

Commit

Permalink
Add Merchant Callback QR (#13)
Browse files Browse the repository at this point in the history
* Add Merchant Callback QR
  • Loading branch information
tomas-zijdemans-vipps authored Dec 29, 2023
1 parent 39a75cb commit fb7c2b3
Show file tree
Hide file tree
Showing 7 changed files with 420 additions and 1 deletion.
58 changes: 58 additions & 0 deletions sample_code/qr_callback_sample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import "https://deno.land/std@0.209.0/dotenv/load.ts";
import { Client } from "../src/mod.ts";

// First, get your API keys from https://portal.vipps.no/
// Here we assume they are stored in a .env file, see .env.example
const clientId = Deno.env.get("CLIENT_ID") || "";
const clientSecret = Deno.env.get("CLIENT_SECRET") || "";

const merchantSerialNumber = Deno.env.get("MERCHANT_SERIAL_NUMBER") || "";
const subscriptionKey = Deno.env.get("SUBSCRIPTION_KEY") || "";

// Create a client
const client = Client({
merchantSerialNumber,
subscriptionKey,
useTestMode: true,
retryRequests: false,
});

// Grab a token
const accessToken = await client.auth.getToken({
clientId,
clientSecret,
subscriptionKey,
});

// Check if the token was retrieved successfully
if (!accessToken.ok) {
console.error("😟 Error retrieving token 😟");
console.error(accessToken.message);
Deno.exit(1);
}

const token = accessToken.data.access_token;

const qrId = crypto.randomUUID();

const qr = await client.callbackQR.create(token, qrId, {
locationDescription: "Kasse 1",
});

// Check if the QR was created successfully
if (!qr.ok) {
console.error("😟 Error creating QR 😟");
console.error(qr.message);
Deno.exit(1);
}

const qrInfo = await client.callbackQR.info(token, qrId);

// Check if the QR was retrieved successfully
if (!qrInfo.ok) {
console.error("😟 Error retrieving QR 😟");
console.error(qrInfo.message);
Deno.exit(1);
}

console.log(qrInfo.data.qrImageUrl);
129 changes: 129 additions & 0 deletions src/apis/qr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { RequestData } from "../types.ts";
import {
MerchantCallbackQr,
MerchantCallbackRequest,
QrErrorResponse,
QrImageFormat,
QrImageSize,
} from "./types/qr_types.ts";

/**
* Factory for creating Merchant callback QR request.
*/
export const callbackQRRequestFactory = {
/**
* Creates a callback QR request.
*
* @param token - The authentication token.
* @param merchantQrId - The merchant defined identifier for a QR code.
* @param body - The request body.
* @returns A `RequestData` object representing the callback QR request.
*/
create(
token: string,
merchantQrId: string,
body: MerchantCallbackRequest,
): RequestData<void, QrErrorResponse> {
return {
url: `/qr/v1/merchant-callback/${merchantQrId}`,
method: "PUT",
body,
token,
};
},
/**
* Returns the QR code represented by the merchantQrId and
* Merchant-Serial-Number provided in the path and header respectively.
* The image format and size of the QR code is defined by the Accept
* and Size headers respectively.
*
* @param token - The authentication token.
* @param merchantQrId - The ID of the merchant callback QR code.
* @param qrImageFormat - The format of the QR code image (default: "SVG").
* @param qrImageSize - The size of the QR code image (optional).
* @returns A `RequestData` object containing the URL, method, and token.
*/
info(
token: string,
merchantQrId: string,
qrImageFormat: QrImageFormat = "SVG",
qrImageSize?: QrImageSize,
): RequestData<MerchantCallbackQr, QrErrorResponse> {
const url = qrImageSize
? `/qr/v1/merchant-callback/${merchantQrId}?QrImageFormat=${qrImageFormat}&QrImageSize=${qrImageSize}`
: `/qr/v1/merchant-callback/${merchantQrId}?QrImageFormat=${qrImageFormat}`;

return { url, method: "GET", token };
},
/**
* Returns all QR codes that matches the provided Merchant-Serial-Number.
*
* @param token - The authentication token.
* @param qrImageFormat - The format of the QR image. Defaults to "SVG".
* @param qrImageSize - The size of the QR image.
* @returns A `RequestData` object containing the URL, method, and token.
*/
list(
token: string,
qrImageFormat: QrImageFormat = "SVG",
qrImageSize?: QrImageSize,
): RequestData<MerchantCallbackQr[], QrErrorResponse> {
const url = qrImageSize
? `/qr/v1/merchant-callback?QrImageFormat=${qrImageFormat}&QrImageSize=${qrImageSize}`
: `/qr/v1/merchant-callback?QrImageFormat=${qrImageFormat}`;

return { url, method: "GET", token };
},
/**
* Deletes the QR code that matches the provided merchantQrId and merchantSerialNumber.
*
* @param token - The authentication token.
* @param merchantQrId - The ID of the merchant QR to delete.
* @returns A `RequestData` object with the URL, method, and token for the delete request.
*/
delete(
token: string,
merchantQrId: string,
): RequestData<void, QrErrorResponse> {
return {
url: `/qr/v1/merchant-callback/${merchantQrId}`,
method: "DELETE",
token,
};
},
/**
* NOTE: This endpoint is only intended for MobilePay PoS customers.
* It will be removed as soon as migration is completed.
*
* This endpoint is for migrating existing MobilePay PoS QR codes from the
* current solution that will end its lifetime. It is meant for merchants
* that have printed QR codes and want them to stay functional for the new
* product offering that will replace the now deprecated solution.
*
* This endpoint will not create a new QR code but rather map the provided
* beaconId with the Merchant-Serial-Number, to make sure the already
* printed QR code can be re-used. When the QR code is scanned by MobilePay
* users, it will result in a callback being sent to the merchant if the
* merchant has registered a webhook for the user.checked-in.v1 event.
*
* The callback will include a MerchantQrId which in this scenario will
* equal the beaconId.
*
* @param token - The authentication token.
* @param beaconId - The beacon ID.
* @param body - The request body.
* @returns The request data.
*/
createMobilePayQR(
token: string,
beaconId: string,
body: MerchantCallbackRequest,
): RequestData<void, QrErrorResponse> {
return {
url: `/qr/v1/merchant-callback/mobilepay/${beaconId}`,
method: "PUT",
body,
token,
};
},
};
135 changes: 135 additions & 0 deletions src/apis/types/qr_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
export type MerchantCallbackRequest = {
/** A description of where the QR code will be located. It will be shown in the app when a user scans the QR code. Examples could be ‘Kasse 1’ , ‘Kiosk’ or ‘Platform 3’. */
locationDescription: string;
};

/**
* @description Requested image format. Supported values: {PNG, SVG}. If not provided, SVG is chosen.
* @example "PNG"
* @default "SVG"
*/
export type QrImageFormat = "PNG" | "SVG";

/**
* @description Dimensions for the image. Only relevant if PNG is chosen as image format.
* @minimum 100
* @maximum 2000
* @example 200. Then 200x200 px is set at dimension for the QR.
*/
export type QrImageSize = number;

export type MerchantCallbackQr = {
/** The merchant serial number (MSN) for the sale unit */
merchantSerialNumber?: string;
/** The merchant defined identifier for a QR code. It will be provided in the callback to the merchant when the QR code has been scanned. */
merchantQrId?: string;
/** A description of where the QR code will be located. It will be shown in the app when a user scans the QR code. Examples could be ‘Kasse 1’ , ‘Kiosk’ or ‘Platform 3’. */
locationDescription?: string;
/**
* The link to the actual QR code.
* @format uri
* @example "https://qr.vipps.no/generate/qr.png?..."
*/
qrImageUrl?: string;
/** The text that is being encoded by the QR code. This is the actual content of the QR code. */
qrContent?: string;
};

export type QrErrorResponse = {
/** @minLength 1 */
type?: string;
/** @minLength 1 */
title: string;
/** @minLength 1 */
detail: string;
/** @minLength 1 */
instance: string;
invalidParams?: {
/** @minLength 1 */
name: string;
/** @minLength 1 */
reason: string;
}[];
};

type OneTimePaymentQrRequest = {
/**
* Url to the Vipps landing page, obtained from ecom/recurring apis
* @example "https://api.vipps.no/dwo-api-application/v1/deeplink/vippsgateway?v=2&token=eyJraWQiO...."
*/
url: string;
};

type OneTimePaymentQrResponse = {
/**
* Link to QR image
* @example "https://qr.vipps.no/generate/qr.png?..."
*/
url: string;
/**
* How many seconds more this QR will be active
* @example 544
*/
expiresIn: number;
};

type MerchantRedirectQrRequest = {
/**
* Merchant supplied Id for QR
* @minLength 1
* @maxLength 128
* @pattern ^[-_+%æøåÆØÅ\w\s]*$
* @example "billboard_1"
*/
id: string | null;
/**
* The target url of the QR (redirect destination)
* @format uri
* @pattern ^https:\/\/[\w\.]+([\w#!:.?+=&%@\-\/]+)?$
* @example "https://example.com/myProduct"
*/
redirectUrl: string | null;
/**
* Optional time-to-live field, given in seconds
* @min 300
* @max 2147483647
* @example 600
*/
ttl?: number | null;
};

type MerchantRedirectQrResponse = {
/**
* Merchant supplied Id for QR
* @pattern ^[-_+%æøåÆØÅ\w\s]*$
* @example "billboard_1"
*/
id: string;
/**
* Link to QR image
* @format uri
* @example "https://qr.vipps.no/generate/qr.png?..."
*/
url: string;
/**
* The target url of the QR (redirect destination)
* @format uri
* @pattern ^https:\/\/[\w\.]+([\w#!:.?+=&%@\-\/]+)?$
* @example "https://example.com/myProduct"
*/
redirectUrl: string;
/**
* Time in seconds until expiration. -1 means no expiration (infinite QR code)
* @example 598
*/
expiresIn?: number;
};

type MerchantRedirectQrUpdateRequest = {
/**
* @format uri
* @pattern ^https:\/\/[\w\.]+([\w#!:.?+=&%@\-\/]+)?$
* @example "https://example.com/myProduct"
*/
redirectUrl: string;
};
2 changes: 2 additions & 0 deletions src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { webhooksRequestFactory } from "./apis/webhooks.ts";
import { checkoutRequestFactory } from "./apis/checkout.ts";
import { agreementRequestFactory } from "./apis/agreement.ts";
import { chargeRequestFactory } from "./apis/charge.ts";
import { callbackQRRequestFactory } from "./apis/qr.ts";

/**
* Creates a client with the specified options.
Expand All @@ -25,6 +26,7 @@ export const Client = (options: ClientConfig) => {
checkout: createApi(client, checkoutRequestFactory),
agreement: createApi(client, agreementRequestFactory),
charge: createApi(client, chargeRequestFactory),
callbackQR: createApi(client, callbackQRRequestFactory),
} satisfies APIClient;

return apiClient;
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export type Credentials = {
} & Pick<ClientConfig, "subscriptionKey">;

export type RequestData<TOk, TErr> = {
method: "GET" | "POST" | "PATCH" | "DELETE";
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
url: string;
headers?: Record<string, string>;
body?: unknown;
Expand Down
5 changes: 5 additions & 0 deletions tests/mod_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ Deno.test("Client - available functions", () => {
assertEquals(typeof client.charge.cancel, "function");
assertEquals(typeof client.charge.capture, "function");
assertEquals(typeof client.charge.refund, "function");
assertEquals(typeof client.callbackQR.create, "function");
assertEquals(typeof client.callbackQR.createMobilePayQR, "function");
assertEquals(typeof client.callbackQR.delete, "function");
assertEquals(typeof client.callbackQR.info, "function");
assertEquals(typeof client.callbackQR.list, "function");
});
Loading

0 comments on commit fb7c2b3

Please sign in to comment.