diff --git a/README.md b/README.md index 9840e098..878879dc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Version -Current version: 4.0.6 +Current version: 4.1.2 ## Platforms supported diff --git a/package.json b/package.json index 61d05ea0..987310b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "remote-pay-cloud", - "version": "4.0.6", + "version": "4.1.2", "description": "Access Clover devices through the cloud.", "keywords": [ "clover", @@ -32,7 +32,7 @@ }, "dependencies": { "eventemitter3": "4.0.7", - "remote-pay-cloud-api": "4.0.4" + "remote-pay-cloud-api": "4.0.5" }, "devDependencies": { "@types/node": "12.6.8", diff --git a/src/com/clover/Version.ts b/src/com/clover/Version.ts index 5802bd50..df79f815 100644 --- a/src/com/clover/Version.ts +++ b/src/com/clover/Version.ts @@ -6,7 +6,7 @@ export class Version { /** * @type {string} - The current version of this library */ - public static CLOVER_CLOUD_SDK_VERSION = "4.0.6"; + public static CLOVER_CLOUD_SDK_VERSION = "4.1.2"; /** * @type {string} - The current SDK name. diff --git a/src/com/clover/remote/client/CloverConnector.ts b/src/com/clover/remote/client/CloverConnector.ts index 629277a1..bd978ccd 100644 --- a/src/com/clover/remote/client/CloverConnector.ts +++ b/src/com/clover/remote/client/CloverConnector.ts @@ -213,6 +213,9 @@ export class CloverConnector implements sdk.remotepay.ICloverConnector { if (request.getExternalReferenceId() != null) { builder.setExternalReferenceId(request.getExternalReferenceId()); } + if (request.getPresentQrcOnly() != null) { + builder.setIsPresentQrcOnly(request.getPresentQrcOnly()) + } builder.setTransactionSettings(transactionSettings); diff --git a/src/com/clover/remote/client/transport/websocket/CloverWebSocketClient.ts b/src/com/clover/remote/client/transport/websocket/CloverWebSocketClient.ts index ac3ef23e..321e891f 100644 --- a/src/com/clover/remote/client/transport/websocket/CloverWebSocketClient.ts +++ b/src/com/clover/remote/client/transport/websocket/CloverWebSocketClient.ts @@ -32,7 +32,7 @@ export class CloverWebSocketClient implements WebSocketListener { return (this.socket ? this.socket.getBufferedAmount(): 0); } - public connect(): void { + public connect(accessToken: string): void { if (this.socket != null) { throw new Error("Socket already created. Must create a new CloverWebSocketClient"); } @@ -41,7 +41,7 @@ export class CloverWebSocketClient implements WebSocketListener { this.socket = this.webSocketImplClass(this.endpoint); // socket.setAutoFlush(true); this.socket.addListener(this); - this.socket.connect(); + this.socket.connect(accessToken); } catch (e) { this.logger.error('connect, connectionError', e); this.listener.connectionError(this, e.message); diff --git a/src/com/clover/remote/client/transport/websocket/WebSocketCloudCloverTransport.ts b/src/com/clover/remote/client/transport/websocket/WebSocketCloudCloverTransport.ts index 12d41eca..79578423 100644 --- a/src/com/clover/remote/client/transport/websocket/WebSocketCloudCloverTransport.ts +++ b/src/com/clover/remote/client/transport/websocket/WebSocketCloudCloverTransport.ts @@ -85,11 +85,12 @@ export class WebSocketCloudCloverTransport extends WebSocketCloverTransport { // We should only enter this block if the browser is IE 11. IE 11 has issues when the first call to the // server is a POST (initializeWithServer). To work-around this we make a GET request // See http://jonnyreeves.co.uk/2013/making-xhr-request-to-https-domains-with-winjs/ for more information. - this.httpSupport.getData(Endpoints.getMerchantEndpoint(this.cloverServer, this.merchantId, this.accessToken), + this.httpSupport.getData(Endpoints.getMerchantEndpoint(this.cloverServer, this.merchantId), (_) => this.obtainWebSocketUrlAndSendPushAlert(), (error) => { this.logger.warn("IE 11 - Initial GET failed.", error); - }); + }, + this.buildAuthorizationHeader(this.accessToken)); } else { // We aren't using IE, make the initial POST. this.obtainWebSocketUrlAndSendPushAlert(); @@ -113,7 +114,7 @@ export class WebSocketCloudCloverTransport extends WebSocketCloverTransport { // Do the notification call. This needs to happen every time we attempt to connect. // It COULD mean that the device gets a notification when the Cloud Pay Display is // already running, but this is not harmful. - let alertEndpoint: string = Endpoints.getAlertDeviceEndpoint(this.cloverServer, this.merchantId, this.accessToken); + let alertEndpoint: string = Endpoints.getAlertDeviceEndpoint(this.cloverServer, this.merchantId); let deviceContactInfo: DeviceContactInfo = new DeviceContactInfo(this.deviceId.replace(/-/g, ""), true); this.httpSupport.postData(alertEndpoint, (data) => this.deviceNotificationSent(data), @@ -124,7 +125,8 @@ export class WebSocketCloudCloverTransport extends WebSocketCloverTransport { null, error && error.status !== 404); }, - deviceContactInfo); + deviceContactInfo, + this.buildAuthorizationHeader(this.accessToken)); } /** @@ -141,7 +143,7 @@ export class WebSocketCloudCloverTransport extends WebSocketCloverTransport { // we will assume an earlier version of the protocol on the server, // and assume that the notification WAS SENT. if (!notificationResponse.hasOwnProperty('sent') || notificationResponse.sent) { - this.webSocketURL = Endpoints.getDeviceWebSocketEndpoint(notificationResponse, this.friendlyId, this.forceConnect, this.merchantId, this.accessToken); + this.webSocketURL = Endpoints.getDeviceWebSocketEndpoint(notificationResponse, this.friendlyId, this.forceConnect, this.merchantId); this.doOptionsCallToAvoid401Error(this.webSocketURL); } else { this.connectionError(this.cloverWebSocketClient, "The device is unreachable or an error has occurred sending the device a notification to start/connect to Cloud Pay Display."); @@ -169,7 +171,8 @@ export class WebSocketCloudCloverTransport extends WebSocketCloverTransport { } this.httpSupport.options(httpUrl, (data, xmlHttpReqImpl) => this.afterOptionsCall(deviceWebSocketEndpoint, xmlHttpReqImpl), - (data, xmlHttpReqImpl) => this.afterOptionsCall(deviceWebSocketEndpoint, xmlHttpReqImpl)); + (data, xmlHttpReqImpl) => this.afterOptionsCall(deviceWebSocketEndpoint, xmlHttpReqImpl), + this.buildAuthorizationHeader(this.accessToken)); } /** @@ -208,7 +211,11 @@ export class WebSocketCloudCloverTransport extends WebSocketCloverTransport { this.notifyConnectionAttemptComplete(); return; // done connecting } - super.initializeWithUri(deviceWebSocketEndpoint); + super.initializeWithUri(deviceWebSocketEndpoint, this.accessToken); + } + + private buildAuthorizationHeader(token: string): object { + return {"Authorization": `Bearer ${token}`}; } /** diff --git a/src/com/clover/remote/client/transport/websocket/WebSocketCloverTransport.ts b/src/com/clover/remote/client/transport/websocket/WebSocketCloverTransport.ts index f03c0b1d..ad37d3a1 100644 --- a/src/com/clover/remote/client/transport/websocket/WebSocketCloverTransport.ts +++ b/src/com/clover/remote/client/transport/websocket/WebSocketCloverTransport.ts @@ -126,8 +126,9 @@ export abstract class WebSocketCloverTransport extends CloverTransport implement * Called from subclasses at the end of the constructor. * * @param deviceEndpoint + * @param accessToken */ - protected initializeWithUri(deviceEndpoint: string): void { // synchronized + protected initializeWithUri(deviceEndpoint: string, accessToken?: string): void { // synchronized if (this.cloverWebSocketClient != null) { if (this.cloverWebSocketClient.isOpen() || this.cloverWebSocketClient.isConnecting()) { return; @@ -136,7 +137,7 @@ export abstract class WebSocketCloverTransport extends CloverTransport implement } } this.cloverWebSocketClient = new CloverWebSocketClient(deviceEndpoint, this, this.webSocketImplClass); - this.cloverWebSocketClient.connect(); + this.cloverWebSocketClient.connect(accessToken); this.logger.info('Connection attempt complete.'); this.notifyConnectionAttemptComplete(); } diff --git a/src/com/clover/util/Endpoints.ts b/src/com/clover/util/Endpoints.ts index 78ee3522..a8d3bb5f 100644 --- a/src/com/clover/util/Endpoints.ts +++ b/src/com/clover/util/Endpoints.ts @@ -90,15 +90,15 @@ export class Endpoints { * The endpoint used to connect to a websocket on the server that will proxy to a device. Used by * remote-pay cloud connectors. * - * @param {any} - notificationResponse - The notification response from COS. + * @param {any} notificationResponse - The notification response from COS. * @param {string} friendlyId - an id used to identify the POS. * @param {boolean} forceConnect - if true, then the attempt will overtake any existing connection * @param {boolean} merchantId - unique identifier for the merchant. - * @param {boolean} accessToken - mid access token * @returns {string} The endpoint used to connect to a websocket on the server that will proxy to a device */ - public static getDeviceWebSocketEndpoint(notificationResponse: any, friendlyId: string, forceConnect: boolean, merchantId: string, accessToken: string): string { + public static getDeviceWebSocketEndpoint(notificationResponse: any, friendlyId: string, forceConnect: boolean, merchantId: string): string { const variables = {}; + // This is not an access token its a device id. variables[Endpoints.WEBSOCKET_TOKEN_KEY] = notificationResponse.token; variables[Endpoints.DOMAIN_KEY] = notificationResponse.host; variables[Endpoints.WEBSOCKET_FRIENDLY_ID_KEY] = encodeURIComponent(friendlyId); @@ -121,16 +121,14 @@ export class Endpoints { * * @param {string} domain - the clover server. EX: https://www.clover.com, http://localhost:9000 * @param {string} merchantId - the id of the merchant to use when getting the merchant. - * @param {string} accessToken - the OAuth token used when accessing the server * @returns {string} endpoint - the url to use to retrieve the merchant */ - public static getMerchantEndpoint(domain: string, merchantId: string, accessToken: string): string { + public static getMerchantEndpoint(domain: string, merchantId: string): string { var variables = {}; variables[Endpoints.MERCHANT_V3_KEY] = merchantId; - variables[Endpoints.ACCESS_TOKEN_KEY] = accessToken; variables[Endpoints.DOMAIN_KEY] = domain; - let merchantEndpointPath: string = Endpoints.DOMAIN_PATH + Endpoints.MERCHANT_V3_PATH + Endpoints.ACCESS_TOKEN_SUFFIX; + let merchantEndpointPath: string = Endpoints.DOMAIN_PATH + Endpoints.MERCHANT_V3_PATH; return Endpoints.setVariables(merchantEndpointPath, variables); } @@ -146,10 +144,9 @@ export class Endpoints { public static getDevicesEndpoint(domain: string, merchantId: string, accessToken: string): string { var variables = {}; variables[Endpoints.MERCHANT_V3_KEY] = merchantId; - variables[Endpoints.ACCESS_TOKEN_KEY] = accessToken; variables[Endpoints.DOMAIN_KEY] = domain; - let devicesEndpointPath: string = Endpoints.DOMAIN_PATH + Endpoints.DEVICE_PATH + Endpoints.ACCESS_TOKEN_SUFFIX; + let devicesEndpointPath: string = Endpoints.DOMAIN_PATH + Endpoints.DEVICE_PATH; return Endpoints.setVariables(devicesEndpointPath, variables); } @@ -160,13 +157,12 @@ export class Endpoints { * @param {string} accessToken - the OAuth token used when accessing the server * @returns {string} endpoint - the url to use alert a device that we want to communicate with it */ - public static getAlertDeviceEndpoint(domain: string, merchantId: string, accessToken: string): string { + public static getAlertDeviceEndpoint(domain: string, merchantId: string): string { var variables = {}; variables[Endpoints.MERCHANT_V3_KEY] = merchantId; - variables[Endpoints.ACCESS_TOKEN_KEY] = accessToken; variables[Endpoints.DOMAIN_KEY] = domain; - let alertDeviceEndpointPath: string = Endpoints.DOMAIN_PATH + Endpoints.REMOTE_PAY_PATH + Endpoints.ACCESS_TOKEN_SUFFIX; + let alertDeviceEndpointPath: string = Endpoints.DOMAIN_PATH + Endpoints.REMOTE_PAY_PATH; return Endpoints.setVariables(alertDeviceEndpointPath, variables); } diff --git a/src/com/clover/util/HttpSupport.ts b/src/com/clover/util/HttpSupport.ts index ba77f5ba..97fd62fc 100644 --- a/src/com/clover/util/HttpSupport.ts +++ b/src/com/clover/util/HttpSupport.ts @@ -1,4 +1,5 @@ import {Logger} from '../remote/client/util/Logger'; +import {addHeaders} from "./addHeaders"; /** * Interface used to abstract implementation details to allow for NodeJS and @@ -68,7 +69,7 @@ export class HttpSupport { /** * Make the REST call to get the data */ - public doXmlHttp(method: string, endpoint: string, onDataLoaded: Function, onError: Function): void { + public doXmlHttp(method: string, endpoint: string, onDataLoaded: Function, onError: Function, additionalHeaders?: any): void { const xmlHttp = new this.xmlHttpImplClass(); this.setXmlHttpCallback(xmlHttp, endpoint, onDataLoaded, onError); xmlHttp.open(method, endpoint, true); @@ -77,7 +78,7 @@ export class HttpSupport { if (typeof(navigator) !== "undefined" && navigator.userAgent.search("Firefox")) { xmlHttp.setRequestHeader("Accept", "*/*"); } - + addHeaders(additionalHeaders, xmlHttp); xmlHttp.send(); } @@ -86,13 +87,7 @@ export class HttpSupport { this.setXmlHttpCallback(xmlHttp, endpoint, onDataLoaded, onError); xmlHttp.open(method, endpoint, true); - if (additionalHeaders) { - for (var key in additionalHeaders) { - if (additionalHeaders.hasOwnProperty(key)) { - xmlHttp.setRequestHeader(key, additionalHeaders[key]); - } - } - } + addHeaders(additionalHeaders, xmlHttp); if (sendData) { xmlHttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); var sendDataStr = JSON.stringify(sendData); @@ -113,15 +108,15 @@ export class HttpSupport { /** * Make the REST call to get the data */ - public getData(endpoint: string, onDataLoaded: Function, onError: Function): void { - this.doXmlHttp("GET", endpoint, onDataLoaded, onError) + public getData(endpoint: string, onDataLoaded: Function, onError: Function, additionalHeaders?: object): void { + this.doXmlHttp("GET", endpoint, onDataLoaded, onError, additionalHeaders) } /** * Make the REST call to get the data */ - public options(endpoint: string, onDataLoaded: Function, onError: Function): void { - this.doXmlHttp("OPTIONS", endpoint, onDataLoaded, onError) + public options(endpoint: string, onDataLoaded: Function, onError: Function, additionalHeaders?: object): void { + this.doXmlHttp("OPTIONS", endpoint, onDataLoaded, onError, additionalHeaders) } /** diff --git a/src/com/clover/util/PayIntent/Builder.ts b/src/com/clover/util/PayIntent/Builder.ts index 78907023..dc6309b2 100644 --- a/src/com/clover/util/PayIntent/Builder.ts +++ b/src/com/clover/util/PayIntent/Builder.ts @@ -50,6 +50,7 @@ export namespace PayIntent { private transactionSettings: sdk.payments.TransactionSettings; private passThroughValues: object; private externalReferenceId: string; + private isPresentQrcOnly: boolean; public static buildTransactionSettingsFromPayIntent(payIntent: sdk.remotemessage.PayIntent): sdk.payments.TransactionSettings { let transactionSettings: sdk.payments.TransactionSettings = new sdk.payments.TransactionSettings(); @@ -310,6 +311,11 @@ export namespace PayIntent { return this; } + public setIsPresentQrcOnly(isPresentQrcOnly: boolean): Builder { + this.isPresentQrcOnly = isPresentQrcOnly; + return this; + } + public build(): sdk.remotemessage.PayIntent { let payIntent: sdk.remotemessage.PayIntent = new sdk.remotemessage.PayIntent(); payIntent.setAction(this.action); @@ -353,6 +359,8 @@ export namespace PayIntent { payIntent.setPassThroughValues(this.passThroughValues); payIntent.setExternalReferenceId(this.externalReferenceId); + payIntent.setIsPresentQrcOnly(this.isPresentQrcOnly); + return payIntent; } } diff --git a/src/com/clover/util/addHeaders.ts b/src/com/clover/util/addHeaders.ts new file mode 100644 index 00000000..80ad9909 --- /dev/null +++ b/src/com/clover/util/addHeaders.ts @@ -0,0 +1,9 @@ +export const addHeaders = (headers, xmlHttp) => { + if (headers) { + for (let key in headers) { + if (headers.hasOwnProperty(key)) { + xmlHttp.setRequestHeader(key, headers[key]); + } + } + } +} \ No newline at end of file diff --git a/src/com/clover/websocket/BrowserWebSocketImpl.ts b/src/com/clover/websocket/BrowserWebSocketImpl.ts index 32065510..facfe35f 100644 --- a/src/com/clover/websocket/BrowserWebSocketImpl.ts +++ b/src/com/clover/websocket/BrowserWebSocketImpl.ts @@ -15,10 +15,12 @@ export class BrowserWebSocketImpl extends CloverWebSocketInterface { * * @override * @param endpoint - the url that will connected to + * @param accessToken - Here the access token is passed as a second param to `new WebSocket()` and will be read + * by the support server as a "subprotocol" in the Sec-WebSocket-Protocol header value. * @returns {WebSocket} - the specific implementation of a websocket */ - public createWebSocket(endpoint: string): any { - return new WebSocket(endpoint); + public createWebSocket(endpoint: string, accessToken?: string): any { + return new WebSocket(endpoint, accessToken); } /** diff --git a/src/com/clover/websocket/CloverWebSocketInterface.ts b/src/com/clover/websocket/CloverWebSocketInterface.ts index d6e52f1b..ba8043d2 100644 --- a/src/com/clover/websocket/CloverWebSocketInterface.ts +++ b/src/com/clover/websocket/CloverWebSocketInterface.ts @@ -38,11 +38,12 @@ export abstract class CloverWebSocketInterface { * will not be properly attached before events begin firing. * * @param endpoint - the uri to connect to + * @param accessToken - the access token that is used to ensure the merchant and device are associated to the token */ - public abstract createWebSocket(endpoint: string): any; + public abstract createWebSocket(endpoint: string, accessToken): any; - public connect(): CloverWebSocketInterface { - this.webSocket = this.createWebSocket(this.endpoint); + public connect(accessToken: string): CloverWebSocketInterface { + this.webSocket = this.createWebSocket(this.endpoint, accessToken); if (typeof this.webSocket["addEventListener"] !== "function") { this.logger.error("FATAL: The websocket implementation being used must have an 'addEventListener' function. Either use a supported websocket implementation (https://www.npmjs.com/package/ws) or override the connect method on CloverWebSocketInterface."); } else {