From 6301901cb547cdb9ab3e4d5eccb454376318e981 Mon Sep 17 00:00:00 2001 From: Tomas Zijdemans <113360400+tomas-zijdemans-vipps@users.noreply.github.com> Date: Wed, 24 Jan 2024 09:32:01 +0100 Subject: [PATCH] Userinfoapi (#35) * BREAKING: Move QR paths * Add Userinfo api --- sample_code/user_sample.ts | 103 ++++++++++++++++++++++++ src/apis/types/user_types.ts | 149 +++++++++++++++++++++++++++++++++++ src/apis/user.ts | 15 ++++ src/mod.ts | 21 +++-- tests/api_proxy_test.ts | 3 +- tests/checkout_test.ts | 22 ++++-- tests/epayment_test.ts | 6 +- tests/error_test.ts | 6 +- tests/fetch_test.ts | 14 +--- tests/mod_test.ts | 32 ++++---- tests/user_test.ts | 19 +++++ 11 files changed, 345 insertions(+), 45 deletions(-) create mode 100644 sample_code/user_sample.ts create mode 100644 src/apis/types/user_types.ts create mode 100644 src/apis/user.ts create mode 100644 tests/user_test.ts diff --git a/sample_code/user_sample.ts b/sample_code/user_sample.ts new file mode 100644 index 0000000..b2a80df --- /dev/null +++ b/sample_code/user_sample.ts @@ -0,0 +1,103 @@ +import "https://deno.land/std@0.212.0/dotenv/load.ts"; +// Add back in when the API has been released +// import { Client } from "https://deno.land/x/vipps_mobilepay_sdk@0.8.0/mod.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.error); + Deno.exit(1); +} + +const token = accessToken.data.access_token; + +// Create a payment with profile flow +const payment = await client.payment.create(token, { + amount: { + currency: "NOK", + value: 1000, // This value equals 10 NOK + }, + paymentMethod: { type: "WALLET" }, + customer: { phoneNumber: "4712345678" }, + returnUrl: `https://yourwebsite.com/redirect`, + userFlow: "WEB_REDIRECT", + paymentDescription: "One pair of socks", + profile: { scope: "name phoneNumber address birthDate" }, +}); + +// Check if the payment was created successfully +if (!payment.ok) { + console.error("😟 Error creating payment 😟"); + console.error(payment.error); + Deno.exit(1); +} +console.log("πŸŽ‰ Payment created successfully!"); + +// Force approve the payment, for testing purposes. +// This requires the customer to have cards registered in Vipps MT +const approve = await client.payment.forceApprove( + token, + payment.data.reference, + { customer: { phoneNumber: "4712345678" } }, +); + +// Check if the payment was approved successfully +if (!approve.ok) { + console.error("😟 Error approving payment 😟"); + console.error(approve.error); + Deno.exit(1); +} + +// Retrive the payment +const paymentInfo = await client.payment.info(token, payment.data.reference); + +// Check if the payment was retrieved successfully +if (!paymentInfo.ok) { + console.error("😟 Error retriving payment 😟"); + console.error(paymentInfo.error); + Deno.exit(1); +} + +const sub = paymentInfo.data.profile.sub; + +if (!sub) { + console.error("😟 Error retriving sub 😟"); + Deno.exit(1); +} + +// Retrive the user info +const userInfo = await client.user.info(token, sub); + +// Check if the user info was retrieved successfully +if (!userInfo.ok) { + console.error("😟 Error retriving user info 😟"); + console.error(userInfo.error); + Deno.exit(1); +} + +console.log("πŸŽ‰ User info retrieved successfully!"); +console.log(userInfo.data); diff --git a/src/apis/types/user_types.ts b/src/apis/types/user_types.ts new file mode 100644 index 0000000..2f76360 --- /dev/null +++ b/src/apis/types/user_types.ts @@ -0,0 +1,149 @@ +/** + * Represents an error response for user information. + */ +export type UserInfoError = { + /** + * A URI reference that identifies the problem type. + * @example "https://example.com/validation-error" + */ + type?: string; + /** + * A short, human-readable summary of the problem type. It will not change from occurrence to occurrence of the problem. + * @example "Your request parameters didn't validate." + */ + title?: string; + /** + * The HTTP status code. + * @example 400 + */ + status?: string; + /** + * A human-readable explanation specific to this occurrence of the problem. + * @example "The request body contains one or more errors" + */ + detail?: string; + /** + * An ID that can help when troubleshooting. + * @example "123e4567-e89b-12d3-a456-426655440000" + */ + instance?: string; + /** + * Additional information related to the error. + */ + extraInfo?: Record; +}; + +/** + * Represents the user information. + */ +export type UserInfo = { + /** Contains the user's preferred (default) address. */ + address?: UserInfoAddress; + /** Contains an array with the user's non-default addresses, if any. This list can contain an address with the address_type home, work, and/or other, if the user has registered them in the Vipps app */ + other_addresses?: UserInfoAddress[]; + /** + * The user's birthday formatted as YYYY-MM-DD + * @example "2000-12-31" + */ + birthdate?: string; + /** + * The user's email address. + * @example "user@example.com" + */ + email?: string; + /** + * Boolean value indicating whether the user's email address is verified or not. + * @example true + */ + email_verified?: boolean; + /** + * Surname(s) or last name(s) of the user. + * @example "Lovelace" + */ + family_name?: string; + /** + * Given name(s) or first name(s) of the user. Note that in some cultures, people can have multiple given names; all can be present, with the names being separated by space characters. + * @example "Ada" + */ + given_name?: string; + /** + * The user's full name in displayable form including all name parts, possibly including titles and suffixes, ordered according to the user's locale and preferences. + * @example "Ada Lovelace" + */ + name?: string; + /** + * National identity number. + * For Norway this is the "fΓΈdselsnummer": 11 digits. + * The format is "YYYYMMDD" + five digits. + * See https://www.skatteetaten.no/en/person/foreign/norwegian-identification-number/national-identity-number/ + * @pattern ^\d{11}$ + * @example "09057517287" + */ + nin?: string; + /** + * The user's telephone number. + * The format is MSISDN: Digits only: Country code and subscriber + * number, but no prefix. + * See https://en.wikipedia.org/wiki/MSISDN + * @pattern ^\d{15}$ + * @example "4791234567" + */ + phone_number?: string; + /** + * Session identifier: This represents a session of a User Agent or + * device. Currently not in use. + * @example "7d78a726-af92-499e-b857-de263ef9a969" + */ + sid?: string; + /** + * Subject: Unique identifier for the user. + * The sub is based on the user's national identity number (NIN) and + * does not change (except in very special cases). + * The `sub` is the same when the user logs in again and re-consents. + * @example "c06c4afe-d9e1-4c5d-939a-177d752a0944" + */ + sub?: string; +}; + +/** + * Represents the address information of a user. + */ +export type UserInfoAddress = { + /** + * Address type is either `home`, `work` or `other`. + * @example "home" + */ + address_type?: string; + /** + * Two letter country code + * @format ^[A-Z]{2}$ + * @example "NO" + */ + country?: string; + /** + * True if this is the default address + * @example true + */ + default?: boolean; + /** + * The user's address as a formatted string + * @example "Robert Levins gate 5 + * 0154 Oslo" + */ + formatted?: string; + /** + * Postal code + * @example "0154" + */ + postal_code?: string; + /** + * The user's region (typically a county, town or city) + * @example "Oslo" + */ + region?: string; + /** + * The user's street address + * @example "Robert Levins gate 5" + */ + street_address?: string; +}; diff --git a/src/apis/user.ts b/src/apis/user.ts new file mode 100644 index 0000000..e99e816 --- /dev/null +++ b/src/apis/user.ts @@ -0,0 +1,15 @@ +import { RequestData } from "../types.ts"; +import { UserInfo, UserInfoError } from "./types/user_types.ts"; + +/** + * Factory object for creating Userinfo API requests. + */ +export const userRequestFactory = { + info(token: string, sub: string): RequestData { + return { + url: `/vipps-userinfo-api/userinfo/${sub}`, + method: "GET", + token, + }; + }, +} as const; diff --git a/src/mod.ts b/src/mod.ts index 903ba58..bfd84d8 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -2,16 +2,19 @@ import { ClientConfig } from "./types.ts"; import { baseClient } from "./base_client.ts"; import { ApiClient, createApi } from "./api_proxy.ts"; import { authRequestFactory } from "./apis/auth.ts"; -import { ePaymentRequestFactory } from "./apis/epayment.ts"; -import { webhooksRequestFactory } from "./apis/webhooks.ts"; import { checkoutRequestFactory } from "./apis/checkout.ts"; -import { agreementRequestFactory } from "./apis/recurring.ts"; -import { chargeRequestFactory } from "./apis/recurring.ts"; +import { ePaymentRequestFactory } from "./apis/epayment.ts"; +import { loginRequestFactory } from "./apis/login.ts"; import { callbackQRRequestFactory, redirectQRRequestFactory, } from "./apis/qr.ts"; -import { loginRequestFactory } from "./apis/login.ts"; +import { + agreementRequestFactory, + chargeRequestFactory, +} from "./apis/recurring.ts"; +import { userRequestFactory } from "./apis/user.ts"; +import { webhooksRequestFactory } from "./apis/webhooks.ts"; /** * Export all API types, for convenience. All exported types are @@ -23,6 +26,7 @@ export type * from "./apis/types/epayment_types.ts"; export type * from "./apis/types/login_types.ts"; export type * from "./apis/types/qr_types.ts"; export type * from "./apis/types/recurring_types.ts"; +export type * from "./apis/types/user_types.ts"; export type * from "./apis/types/webhooks_types.ts"; /** @@ -37,15 +41,18 @@ export const Client = (options: ClientConfig) => { // Create the API client const apiClient = { auth: createApi(client, authRequestFactory), - callbackQR: createApi(client, callbackQRRequestFactory), checkout: createApi(client, checkoutRequestFactory), login: createApi(client, loginRequestFactory), payment: createApi(client, ePaymentRequestFactory), + qr: { + callback: createApi(client, callbackQRRequestFactory), + redirect: createApi(client, redirectQRRequestFactory), + }, recurring: { charge: createApi(client, chargeRequestFactory), agreement: createApi(client, agreementRequestFactory), }, - redirectQR: createApi(client, redirectQRRequestFactory), + user: createApi(client, userRequestFactory), webhook: createApi(client, webhooksRequestFactory), } satisfies ApiClient; diff --git a/tests/api_proxy_test.ts b/tests/api_proxy_test.ts index 446b653..4bd8111 100644 --- a/tests/api_proxy_test.ts +++ b/tests/api_proxy_test.ts @@ -32,8 +32,7 @@ Deno.test("createApi - Should return an error if method is not in factory", asyn }, }; - // deno-lint-ignore no-explicit-any - const api = createApi(client, factory) as any; + const api = createApi(client, factory) as unknown as { bar(): Promise }; try { await api.bar(); diff --git a/tests/checkout_test.ts b/tests/checkout_test.ts index 05b5e7d..8839318 100644 --- a/tests/checkout_test.ts +++ b/tests/checkout_test.ts @@ -1,6 +1,11 @@ import { checkoutRequestFactory } from "../src/apis/checkout.ts"; import { CheckoutInitiateSessionRequest } from "../src/apis/types/checkout_types.ts"; -import { assertEquals, assertExists, assertNotEquals } from "./test_deps.ts"; +import { + assert, + assertEquals, + assertExists, + assertNotEquals, +} from "./test_deps.ts"; Deno.test("create - should return the correct request data", () => { const client_id = "your_client_id"; @@ -58,11 +63,18 @@ Deno.test("create - should fill in missing properties", () => { client_id, client_secret, body, - // deno-lint-ignore no-explicit-any - ) as any; + ); - assertExists(requestData.body.transaction.reference); - assertExists(requestData.body.merchantInfo.callbackAuthorizationToken); + assertExists(requestData.body); + assert("transaction" in requestData.body); + assert( + "reference" in (requestData.body.transaction as Record), + ); + assert("merchantInfo" in requestData.body); + assert( + "callbackAuthorizationToken" in + (requestData.body.merchantInfo as Record), + ); }); Deno.test("info - should return the correct request data", () => { diff --git a/tests/epayment_test.ts b/tests/epayment_test.ts index 87908d0..7ec17b5 100644 --- a/tests/epayment_test.ts +++ b/tests/epayment_test.ts @@ -59,10 +59,10 @@ Deno.test("ePayment - create - Should fill in missing props", () => { userFlow: "WEB_REDIRECT", paymentDescription: "One pair of socks", }, - // deno-lint-ignore no-explicit-any - ) as any; + ); - assertExists(requestData.body.reference); + assertExists(requestData.body); + assertExists("reference" in requestData.body); }); Deno.test("ePayment - info - Should have correct url and header", async () => { diff --git a/tests/error_test.ts b/tests/error_test.ts index 6d6780b..a9a48d1 100644 --- a/tests/error_test.ts +++ b/tests/error_test.ts @@ -1,6 +1,7 @@ import { AccessTokenError } from "../src/apis/types/auth_types.ts"; import { parseError } from "../src/errors.ts"; import { Client, RecurringErrorFromAzure } from "../src/mod.ts"; +import { assert, assertExists } from "./test_deps.ts"; import { assertEquals, mf } from "./test_deps.ts"; Deno.test("parseError - Should return correct error message for connection error", () => { @@ -52,10 +53,11 @@ Deno.test("parseError - Should return correct error message for AccessTokenError Deno.test("parseError should return correct error message for unknown error", () => { const error = "Unknown error"; - // deno-lint-ignore no-explicit-any - const result: any = parseError(error); + const result = parseError(error); assertEquals(result.ok, false); + assertExists(result.error); + assert("message" in result.error); assertEquals(result.error.message, "Unknown error"); }); diff --git a/tests/fetch_test.ts b/tests/fetch_test.ts index dca7c18..70a3147 100644 --- a/tests/fetch_test.ts +++ b/tests/fetch_test.ts @@ -29,8 +29,7 @@ Deno.test("fetchJSON - Returns parseError on Bad Request", async () => { }); const request = new Request("https://example.com/api"); - // deno-lint-ignore no-explicit-any - const result = await fetchJSON(request) as any; + const result = await fetchJSON(request); assertEquals(result.ok, false); mf.reset(); }); @@ -46,10 +45,8 @@ Deno.test("fetchJSON - Returns parseError on Forbidden", async () => { }); const request = new Request("https://example.com/api"); - // deno-lint-ignore no-explicit-any - const result = await fetchJSON(request) as any; + const result = await fetchJSON(request); assertEquals(result.ok, false); - assert(result.error.message !== undefined); mf.reset(); }); @@ -85,8 +82,7 @@ Deno.test("fetchJSON - Catch JSON", async () => { }); const request = new Request("https://example.com/api"); - // deno-lint-ignore no-explicit-any - const result = await fetchJSON(request) as any; + const result = await fetchJSON(request); assertEquals(result.ok, true); mf.reset(); @@ -102,10 +98,8 @@ Deno.test("fetchJSON - Catch Empty Response", async () => { }); const request = new Request("https://example.com/api"); - // deno-lint-ignore no-explicit-any - const result = await fetchJSON(request) as any; + const result = await fetchJSON(request); assertEquals(result.ok, true); - assertEquals(result.data, {}); mf.reset(); }); diff --git a/tests/mod_test.ts b/tests/mod_test.ts index c2d52c3..e2d14e9 100644 --- a/tests/mod_test.ts +++ b/tests/mod_test.ts @@ -5,6 +5,9 @@ Deno.test("Client - available functions", () => { const client = Client({ merchantSerialNumber: "", subscriptionKey: "" }); assertEquals(typeof client.auth.getToken, "function"); + assertEquals(typeof client.checkout.create, "function"); + assertEquals(typeof client.checkout.info, "function"); + assertEquals(typeof client.login.discovery, "function"); assertEquals(typeof client.payment.create, "function"); assertEquals(typeof client.payment.info, "function"); assertEquals(typeof client.payment.cancel, "function"); @@ -12,11 +15,16 @@ Deno.test("Client - available functions", () => { assertEquals(typeof client.payment.refund, "function"); assertEquals(typeof client.payment.forceApprove, "function"); assertEquals(typeof client.payment.history, "function"); - assertEquals(typeof client.webhook.list, "function"); - assertEquals(typeof client.webhook.register, "function"); - assertEquals(typeof client.webhook.delete, "function"); - assertEquals(typeof client.checkout.create, "function"); - assertEquals(typeof client.checkout.info, "function"); + assertEquals(typeof client.qr.redirect.create, "function"); + assertEquals(typeof client.qr.redirect.info, "function"); + assertEquals(typeof client.qr.redirect.list, "function"); + assertEquals(typeof client.qr.redirect.delete, "function"); + assertEquals(typeof client.qr.redirect.update, "function"); + assertEquals(typeof client.qr.callback.create, "function"); + assertEquals(typeof client.qr.callback.createMobilePayQR, "function"); + assertEquals(typeof client.qr.callback.delete, "function"); + assertEquals(typeof client.qr.callback.info, "function"); + assertEquals(typeof client.qr.callback.list, "function"); assertEquals(typeof client.recurring.agreement.create, "function"); assertEquals(typeof client.recurring.agreement.info, "function"); assertEquals(typeof client.recurring.agreement.list, "function"); @@ -29,15 +37,7 @@ Deno.test("Client - available functions", () => { assertEquals(typeof client.recurring.charge.cancel, "function"); assertEquals(typeof client.recurring.charge.capture, "function"); assertEquals(typeof client.recurring.charge.refund, "function"); - assertEquals(typeof client.redirectQR.create, "function"); - assertEquals(typeof client.redirectQR.info, "function"); - assertEquals(typeof client.redirectQR.list, "function"); - assertEquals(typeof client.redirectQR.delete, "function"); - assertEquals(typeof client.redirectQR.update, "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"); - assertEquals(typeof client.login.discovery, "function"); + assertEquals(typeof client.webhook.list, "function"); + assertEquals(typeof client.webhook.register, "function"); + assertEquals(typeof client.webhook.delete, "function"); }); diff --git a/tests/user_test.ts b/tests/user_test.ts new file mode 100644 index 0000000..7f0d6ca --- /dev/null +++ b/tests/user_test.ts @@ -0,0 +1,19 @@ +import { userRequestFactory } from "../src/apis/user.ts"; +import { assertEquals } from "./test_deps.ts"; + +Deno.test("userRequestFactory.info should return the correct RequestData object", () => { + const token = "your-token"; + const sub = "your-sub"; + + const expectedRequestData = { + url: "/vipps-userinfo-api/userinfo/your-sub", + method: "GET", + token: "your-token", + }; + + const requestData = userRequestFactory.info(token, sub); + + assertEquals(requestData.url, expectedRequestData.url); + assertEquals(requestData.method, expectedRequestData.method); + assertEquals(requestData.token, expectedRequestData.token); +});