@fat-zebra/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.jest/setup-env-vars.js +2 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +6 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/react/VerifyCard.d.ts +10 -0
  6. package/dist/react/VerifyCard.js +22 -0
  7. package/dist/react/VerifyCard.js.map +1 -0
  8. package/dist/react/index.d.ts +2 -0
  9. package/dist/react/index.js +9 -0
  10. package/dist/react/index.js.map +1 -0
  11. package/dist/react/url.d.ts +26 -0
  12. package/dist/react/url.js +63 -0
  13. package/dist/react/url.js.map +1 -0
  14. package/dist/react/useFatZebra.d.ts +12 -0
  15. package/dist/react/useFatZebra.js +148 -0
  16. package/dist/react/useFatZebra.js.map +1 -0
  17. package/dist/sca/cardinal.d.ts +28 -0
  18. package/dist/sca/cardinal.js +110 -0
  19. package/dist/sca/cardinal.js.map +1 -0
  20. package/dist/sca/eci-mappings.d.ts +6 -0
  21. package/dist/sca/eci-mappings.js +63 -0
  22. package/dist/sca/eci-mappings.js.map +1 -0
  23. package/dist/sca/index.d.ts +38 -0
  24. package/dist/sca/index.js +395 -0
  25. package/dist/sca/index.js.map +1 -0
  26. package/dist/sca/scenarios/enrollment.d.ts +15 -0
  27. package/dist/sca/scenarios/enrollment.js +150 -0
  28. package/dist/sca/scenarios/enrollment.js.map +1 -0
  29. package/dist/sca/scenarios/index.d.ts +2 -0
  30. package/dist/sca/scenarios/index.js +8 -0
  31. package/dist/sca/scenarios/index.js.map +1 -0
  32. package/dist/sca/scenarios/validation.d.ts +15 -0
  33. package/dist/sca/scenarios/validation.js +128 -0
  34. package/dist/sca/scenarios/validation.js.map +1 -0
  35. package/dist/sca/types.d.ts +158 -0
  36. package/dist/sca/types.js +57 -0
  37. package/dist/sca/types.js.map +1 -0
  38. package/dist/shared/api-gateway-client.d.ts +17 -0
  39. package/dist/shared/api-gateway-client.js +106 -0
  40. package/dist/shared/api-gateway-client.js.map +1 -0
  41. package/dist/shared/bridge-client.d.ts +2 -0
  42. package/dist/shared/bridge-client.js +24 -0
  43. package/dist/shared/bridge-client.js.map +1 -0
  44. package/dist/shared/constants.d.ts +6 -0
  45. package/dist/shared/constants.js +19 -0
  46. package/dist/shared/constants.js.map +1 -0
  47. package/dist/shared/event-manager.d.ts +28 -0
  48. package/dist/shared/event-manager.js +15 -0
  49. package/dist/shared/event-manager.js.map +1 -0
  50. package/dist/shared/post-message-client.d.ts +47 -0
  51. package/dist/shared/post-message-client.js +107 -0
  52. package/dist/shared/post-message-client.js.map +1 -0
  53. package/dist/shared/types.d.ts +142 -0
  54. package/dist/shared/types.js +41 -0
  55. package/dist/shared/types.js.map +1 -0
  56. package/dist/shared/util.d.ts +6 -0
  57. package/dist/shared/util.js +96 -0
  58. package/dist/shared/util.js.map +1 -0
  59. package/fatzebra-sdk-1.0.0.tgz +0 -0
  60. package/index.js +0 -0
  61. package/jest.config.js +16 -0
  62. package/package.json +33 -0
  63. package/src/index.ts +9 -0
  64. package/src/react/VerifyCard.tsx +36 -0
  65. package/src/react/index.ts +3 -0
  66. package/src/react/url.ts +97 -0
  67. package/src/react/useFatZebra.ts +154 -0
  68. package/src/sca/__tests__/eci-mappings.test.ts +35 -0
  69. package/src/sca/__tests__/index.test.ts +514 -0
  70. package/src/sca/cardinal.ts +136 -0
  71. package/src/sca/eci-mappings.ts +63 -0
  72. package/src/sca/index.ts +329 -0
  73. package/src/sca/scenarios/enrollment.ts +164 -0
  74. package/src/sca/scenarios/index.ts +2 -0
  75. package/src/sca/scenarios/validation.ts +142 -0
  76. package/src/sca/types.ts +206 -0
  77. package/src/shared/api-gateway-client.ts +71 -0
  78. package/src/shared/bridge-client.ts +26 -0
  79. package/src/shared/constants.ts +19 -0
  80. package/src/shared/event-manager.ts +44 -0
  81. package/src/shared/post-message-client.test.ts +73 -0
  82. package/src/shared/post-message-client.ts +125 -0
  83. package/src/shared/types.test.ts +65 -0
  84. package/src/shared/types.ts +182 -0
  85. package/src/shared/util.test.ts +164 -0
  86. package/src/shared/util.ts +98 -0
  87. package/tests/helpers/api-gateway-mock.ts +46 -0
  88. package/tsconfig.json +22 -0
  89. package/yarn-error.log +2374 -0
@@ -0,0 +1,142 @@
1
+ import { ScaErrorCode, VEResEnrolled, PARes } from '../types'
2
+
3
+ interface ValidationScenario {
4
+ description: string
5
+ threedsVersion: '1' | '2',
6
+ reasonCode: string
7
+ veresEnrolled: VEResEnrolled
8
+ pares: PARes | null // null is for challenge
9
+ outcome: {
10
+ authenticationType?: 'bypass' | 'frictionless' | 'challenge',
11
+ success: boolean,
12
+ errorCode?: string
13
+ }
14
+ }
15
+
16
+ const validationScenarios: ValidationScenario[] = [
17
+ /***************************************************************/
18
+ /********************** 3DS2 scenarios *************************/
19
+ /***************************************************************/
20
+ {
21
+ // Successful Step-Up Authentication
22
+ description: 'Successful Step-Up Authentication',
23
+ threedsVersion: '2',
24
+ reasonCode: '100',
25
+ veresEnrolled: null,
26
+ pares: PARes.SUCCESS,
27
+ outcome: {
28
+ authenticationType: 'challenge',
29
+ success: true // liability shift
30
+ },
31
+ },
32
+ {
33
+ // Unsuccessful Step-Up Authentication
34
+ description: 'Unsuccessful Step-Up Authentication',
35
+ threedsVersion: '2',
36
+ reasonCode: '476',
37
+ veresEnrolled: null,
38
+ pares: PARes.CANCELED,
39
+ outcome: {
40
+ authenticationType: 'challenge',
41
+ success: false, // no liability shift
42
+ errorCode: ScaErrorCode.UNSUCCESSFUL_STEPUP_AUTHENTICATION
43
+ }
44
+ },
45
+ {
46
+ // Unavailable Step-Up Authentication
47
+ description: 'Unavailable Step-Up Authentication',
48
+ threedsVersion: '2',
49
+ reasonCode: '100',
50
+ veresEnrolled: null,
51
+ pares: PARes.NOT_COMPLETED,
52
+ outcome: {
53
+ authenticationType: 'challenge',
54
+ success: false, // no liability shift
55
+ errorCode: ScaErrorCode.UNAVAILABLE_STEPUP_AUTHENTICATION
56
+ }
57
+ },
58
+ /***************************************************************/
59
+ /********************** 3DS1 scenarios *************************/
60
+ /***************************************************************/
61
+ {
62
+ // Successful Step-Up Authentication
63
+ description: 'Successful Authentication',
64
+ threedsVersion: '1',
65
+ reasonCode: '100',
66
+ veresEnrolled: null,
67
+ pares: PARes.SUCCESS,
68
+ outcome: {
69
+ authenticationType: 'challenge',
70
+ success: true // liability shift
71
+ },
72
+ },
73
+ {
74
+ // Incomplete Authentication
75
+ description: 'Incomplete Authentication',
76
+ threedsVersion: '1',
77
+ reasonCode: '100',
78
+ veresEnrolled: null,
79
+ pares: PARes.NOT_COMPLETED,
80
+ outcome: {
81
+ authenticationType: 'challenge',
82
+ success: false,
83
+ errorCode: ScaErrorCode.UNAVAILABLE_STEPUP_AUTHENTICATION
84
+ }
85
+ },
86
+ {
87
+ // Attempts Processing
88
+ description: 'Attempts Processing',
89
+ threedsVersion: '1',
90
+ reasonCode: '100',
91
+ veresEnrolled: null,
92
+ pares: PARes.ATTEMPTED,
93
+ outcome: {
94
+ authenticationType: 'challenge',
95
+ success: true
96
+ }
97
+ },
98
+ {
99
+ // Unsuccessful Authentication
100
+ description: 'Unsuccessful Authentication',
101
+ threedsVersion: '1',
102
+ reasonCode: '476',
103
+ veresEnrolled: null,
104
+ pares: PARes.CANCELED,
105
+ outcome: {
106
+ authenticationType: 'challenge',
107
+ success: false,
108
+ errorCode: ScaErrorCode.UNSUCCESSFUL_STEPUP_AUTHENTICATION
109
+ }
110
+ },
111
+ {
112
+ // Successful Authentication But Invalid PARes
113
+ description: 'Successful Authentication But Invalid PARes',
114
+ threedsVersion: '1',
115
+ reasonCode: '476',
116
+ veresEnrolled: null,
117
+ pares: null,
118
+ outcome: {
119
+ authenticationType: 'challenge',
120
+ success: false,
121
+ errorCode: ScaErrorCode.UNSUCCESSFUL_STEPUP_AUTHENTICATION
122
+ }
123
+ },
124
+ {
125
+ // Authentication Error
126
+ description: 'Authentication Error',
127
+ threedsVersion: '1',
128
+ reasonCode: '476',
129
+ veresEnrolled: null,
130
+ pares: null,
131
+ outcome: {
132
+ authenticationType: 'challenge',
133
+ success: false, // no liability shift
134
+ errorCode: ScaErrorCode.UNSUCCESSFUL_STEPUP_AUTHENTICATION
135
+ }
136
+ },
137
+ ]
138
+
139
+ export {
140
+ ValidationScenario,
141
+ validationScenarios
142
+ }
@@ -0,0 +1,206 @@
1
+ enum ScaErrorCode {
2
+ BYPASSED_AUTHENTICATION = '001',
3
+ AUTHENTICATION_NOT_AVAILABLE_ON_LOOKUP = '002',
4
+ UNAVAILABLE_FRICTIONLESS_AUTHENTICATION = '003',
5
+ UNSUCCESSFUL_FRICTIONLESS_AUTHENTICATION = '004',
6
+ REJECTED_FRICTIONLESS_AUTHENTICATION = '005',
7
+ UNSUCCESSFUL_STEPUP_AUTHENTICATION = '006',
8
+ UNAVAILABLE_STEPUP_AUTHENTICATION = '007'
9
+ }
10
+
11
+ enum ChallengeWindowSize {
12
+ SIZE_250X400 = '01',
13
+ SIZE_390X400 = '02',
14
+ SIZE_500X600 = '03',
15
+ SIZE_600X400 = '04',
16
+ SIZE_FULL_PAGE = '05',
17
+ }
18
+
19
+ interface CreateSCASessionRequest {
20
+ amount: number
21
+ currency: string
22
+ hide_card_holder: boolean;
23
+ }
24
+
25
+ interface CreateSCASessionResponse {
26
+ jwt: string
27
+ }
28
+
29
+ interface ThreedsData {
30
+ cavv: string
31
+ par: string
32
+ sli: string
33
+ xid: string
34
+ ver: string
35
+ directoryServerTxnId: string
36
+ threedsVersion: string
37
+ }
38
+
39
+
40
+ interface EnrollSCARequest {
41
+ amount: number
42
+ card_token: string
43
+ currency: string
44
+ reference: string
45
+ verification: string
46
+ session_id: string
47
+ challenge_window_size: ChallengeWindowSize
48
+ hide_card_holder: boolean
49
+ customer?: {
50
+ first_name?: string
51
+ last_name?: string
52
+ email?: string
53
+ address?: string
54
+ city?: string
55
+ postcode?: string
56
+ state?: string
57
+ country?: string
58
+ }
59
+ }
60
+
61
+ interface EnrollSCAResponse {
62
+ aav: string
63
+ acs_url: string
64
+ action: {
65
+ proceed: boolean
66
+ challenge: boolean
67
+ frictionless: boolean
68
+ bypass: boolean
69
+ }
70
+ authentication_path: string
71
+ authentication_result: string
72
+ authentication_status_message: string
73
+ authentication_transaction_id: string
74
+ card_bin: string
75
+ card_type_name: string
76
+ cavv: string
77
+ // payerAuthEnrollReply_commerceIndicator
78
+ // Commerce indicator for cards not enrolled.
79
+ commerce_indicator: CommerceIndicator
80
+ currency: string
81
+ decision: 'ACCEPT' | 'REJECT' | 'ERROR'
82
+ directory_server_txn_id: string
83
+ eci: string
84
+ // payerAuthEnrollReply_veresEnrolled
85
+ // Result of the enrollment check for Asia, Middle East and Africa gateway
86
+ enrolled: VEResEnrolled
87
+ pareq: string
88
+ pares: PARes
89
+ proxy_pan: string
90
+ reason_code: string
91
+ request_id: string
92
+ request_token: string
93
+ ucaf_collection_indicator: string
94
+ version: string // 3DS version
95
+ xid: string
96
+ }
97
+
98
+ enum CommerceIndicator {
99
+ // Card not enrolled, or card type not supported by payer authentication. No liability shift.
100
+ INTERNET = 'internet',
101
+ // JCB card not enrolled, but attempt to authenticate is recorded. Liability shift.
102
+ JS_ATTEMPTED = 'js_attempted',
103
+ // You receive this result if JCB’s directory service is not available. No liability shift.
104
+ JS_FAILURE = 'js_failure',
105
+ // Mastercard card not enrolled. No liability shift.
106
+ SPA = 'spa',
107
+ // Visa card not enrolled, but attempt to authenticate is recorded. Liability shift.
108
+ VBV_ATTEMPTED = 'vbv_attempted',
109
+ // For the payment processors Barclays, Streamline, AIBMS, or FDC Germany, you receive this result if Visa’s directory service is not available. No liability shift.
110
+ VBV_FAILURE = 'vbv_failure'
111
+ }
112
+
113
+ enum VEResEnrolled {
114
+ // Card enrolled. You must authenticate. Liability shift.
115
+ ENROLLED = 'Y',
116
+ // Card not enrolled. Proceed with authorization
117
+ NOT_ENROLLED = 'N',
118
+ // Enable to authenticate regardless of the reason. No liability shift.
119
+ UNABLE = 'U',
120
+ // Authentication was bypassed
121
+ BYPASSED = 'B'
122
+ }
123
+
124
+ enum PARes {
125
+ ATTEMPTED = 'A',
126
+ BYPASSED = 'B',
127
+ CANCELED = 'N',
128
+ REJECTED = 'R',
129
+ NOT_COMPLETED = 'U',
130
+ SUCCESS = 'Y'
131
+ }
132
+
133
+ interface ValidateSCARequest {
134
+ amount: number
135
+ authentication_transaction_id: string
136
+ card_token: string
137
+ currency: string
138
+ pareq: string
139
+ reference: string
140
+ }
141
+
142
+ interface ValidateSCAResponse {
143
+ aav: string
144
+ acs_url: string
145
+ authentication_result: string
146
+ card_bin: string
147
+ cavv: string
148
+ commerce_indicator: CommerceIndicator
149
+ decision: 'ACCEPT' | 'REJECT' | 'ERROR'
150
+ directory_server_txn_id: string
151
+ eci: string
152
+ enrolled: VEResEnrolled
153
+ pareq: string
154
+ pares: PARes
155
+ reason_code: string
156
+ request_id: string
157
+ request_token: string
158
+ ucaf_collection_indicator: string
159
+ xid: string
160
+ version: string // 3DS version
161
+ }
162
+
163
+ interface DecodeSCASessionRequest {
164
+ token: string
165
+ }
166
+
167
+ interface DecodeSCASessionResponse {
168
+ amount: number
169
+ currency_code: string
170
+ processor_transaction_id: string
171
+ consumer_session_id: string
172
+ reference_id: string
173
+ type: string
174
+ action_code: string
175
+ error_number: number
176
+ error_description: string
177
+ errors: string[]
178
+ test: boolean
179
+ }
180
+
181
+ interface GetCardRequest {
182
+ card_token: string
183
+ }
184
+
185
+ interface GetCardResponse {
186
+ bin: string
187
+ }
188
+
189
+ export {
190
+ ScaErrorCode,
191
+ ChallengeWindowSize,
192
+ CreateSCASessionRequest,
193
+ CreateSCASessionResponse,
194
+ EnrollSCARequest,
195
+ EnrollSCAResponse,
196
+ CommerceIndicator,
197
+ VEResEnrolled,
198
+ PARes,
199
+ ValidateSCARequest,
200
+ ValidateSCAResponse,
201
+ DecodeSCASessionRequest,
202
+ DecodeSCASessionResponse,
203
+ GetCardRequest,
204
+ GetCardResponse,
205
+ ThreedsData,
206
+ }
@@ -0,0 +1,71 @@
1
+ import {
2
+ RequestHeaderSdkVersion,
3
+ RequestHeaderMerchantUsername,
4
+ RequestTimeout,
5
+ LongRequestTimeout,
6
+ } from "./constants";
7
+ import axios, { AxiosResponse, AxiosInstance } from "axios";
8
+ import { getSdkVersionNumber } from "./util";
9
+
10
+ const baseUrl = process.env.API_GATEWAY_URL;
11
+
12
+ import * as sca from "../sca/types";
13
+
14
+ export type GatewayClientProps = {
15
+ accessToken: string;
16
+ username: string;
17
+ }
18
+
19
+ class GatewayClient {
20
+ client: AxiosInstance | null;
21
+
22
+ constructor({ accessToken, username }: GatewayClientProps) {
23
+ const headers: { [key: string]: string } = {};
24
+ headers["Authorization"] = `Bearer ${accessToken}`;
25
+ headers[RequestHeaderSdkVersion] = getSdkVersionNumber();
26
+ headers[RequestHeaderMerchantUsername] = username;
27
+
28
+ this.client = axios.create({
29
+ baseURL: baseUrl,
30
+ timeout: RequestTimeout,
31
+ headers,
32
+ responseType: "json",
33
+ transformResponse: [
34
+ function (response) {
35
+ const _response =
36
+ typeof response === "string" ? JSON.parse(response) : response;
37
+ return _response.data;
38
+ },
39
+ ],
40
+ });
41
+ }
42
+
43
+ /**************** SCA/3DS2 /****************/
44
+
45
+ async createSCASession(
46
+ data: sca.CreateSCASessionRequest
47
+ ): Promise<AxiosResponse> {
48
+ return this.client.post("/sca/session", data);
49
+ }
50
+
51
+ async decodeSCASession(
52
+ data: sca.DecodeSCASessionRequest
53
+ ): Promise<AxiosResponse> {
54
+ // return requestBase(`${baseUrl}/sca/decode_session`, 'POST', data)
55
+ return this.client.post("/sca/decode_session", data);
56
+ }
57
+
58
+ async enrolSCA(data: sca.EnrollSCARequest): Promise<AxiosResponse> {
59
+ return this.client.post("/sca/enrollment", data);
60
+ }
61
+
62
+ async validateSCA(data: sca.ValidateSCARequest): Promise<AxiosResponse> {
63
+ return this.client.post("/sca/validation", data);
64
+ }
65
+
66
+ async getCard(data: sca.GetCardRequest): Promise<AxiosResponse> {
67
+ return this.client.get(`/credit_cards/${data.card_token}`);
68
+ }
69
+ }
70
+
71
+ export default GatewayClient;
@@ -0,0 +1,26 @@
1
+ const headlessIframeId = 'fz-hl'
2
+
3
+ const load = (url: string): HTMLIFrameElement => {
4
+ const existing = document.getElementById(headlessIframeId)
5
+
6
+ if (existing) {
7
+ return existing as HTMLIFrameElement
8
+ }
9
+
10
+ const headless = document.createElement('iframe')
11
+ headless.style.visibility = 'hidden'
12
+ headless.style.position = 'absolute'
13
+ headless.style.left = '0px'
14
+ headless.style.top = '0px'
15
+ headless.style.height = '0px'
16
+ headless.style.width = '0px'
17
+ headless.style.border = 'none'
18
+ headless.setAttribute('src', url)
19
+ headless.setAttribute('id', headlessIframeId)
20
+ document.body.appendChild(headless)
21
+ return headless
22
+ }
23
+
24
+ export {
25
+ load
26
+ }
@@ -0,0 +1,19 @@
1
+ // Local storage key naming convension:
2
+ // Hypen-separated string with all letters in lower case.
3
+ const LocalStorageAccessTokenKey = 'fz-access-token'
4
+
5
+ // Request header naming convension:
6
+ // Hyphen-separated string with capitalised 1st letter. Capitalise acronyms
7
+ // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
8
+ const RequestHeaderSdkVersion = 'fz-sdk-version'
9
+ const RequestHeaderMerchantUsername = 'fz-merchant-username'
10
+ const RequestTimeout = 8000 // ms
11
+ const LongRequestTimeout = 15000 // ms
12
+
13
+ export {
14
+ LocalStorageAccessTokenKey,
15
+ RequestHeaderSdkVersion,
16
+ RequestHeaderMerchantUsername,
17
+ RequestTimeout,
18
+ LongRequestTimeout
19
+ }
@@ -0,0 +1,44 @@
1
+ import { PublicEvent } from "./types";
2
+
3
+ interface EventData {
4
+ detail: { data: any; message?: string } | { errors: string[] };
5
+ }
6
+
7
+ interface SuccessEventDetail {
8
+ message: string;
9
+ data: any;
10
+ }
11
+
12
+ interface ErrorEventDetail {
13
+ errors: string[];
14
+ data: any;
15
+ }
16
+
17
+ interface SuccessEventDetail {
18
+ message: string;
19
+ data: any;
20
+ }
21
+
22
+ interface ErrorEventDetail {
23
+ errors: string[];
24
+ data: any;
25
+ }
26
+
27
+ // TODO: CustomEvent requires polyfill for IE.
28
+ const emit = (
29
+ event: PublicEvent,
30
+ data: SuccessEventDetail | ErrorEventDetail,
31
+ target: HTMLElement | Window = window,
32
+ ): void => {
33
+ target.dispatchEvent(new CustomEvent(event, { detail: data }));
34
+ };
35
+
36
+ const on = (
37
+ event: PublicEvent,
38
+ callback: (e: any) => void,
39
+ target: HTMLElement | Window = window,
40
+ ): void => {
41
+ target.addEventListener(event, callback);
42
+ };
43
+
44
+ export { EventData, emit, on };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import {
6
+ PostMessage,
7
+ PostMessageLegacy,
8
+ PostMessageClient
9
+ } from '../../src/shared/post-message-client'
10
+
11
+ describe('PostMessageClient', () => {
12
+
13
+ describe('#setEventListeners', () => {
14
+ const message: PostMessage = {
15
+ channel: 'sca',
16
+ subject: 'test_subject',
17
+ data: {
18
+ action: 'transaction.complete'
19
+ }
20
+ }
21
+
22
+ it('selects the correct handler ', (done) => {
23
+ const document = window.document;
24
+ const iframe = document.createElement('iframe');
25
+ const handler = (data: any) => {
26
+ expect(data.action).toEqual('transaction.complete')
27
+ done();
28
+ };
29
+
30
+ document.body.appendChild(iframe);
31
+
32
+ const client = new PostMessageClient({
33
+ channel: 'sca'
34
+ })
35
+
36
+ client.setEventListeners({
37
+ 'test_subject': handler
38
+ })
39
+
40
+ window.postMessage(message, '*')
41
+ })
42
+ })
43
+
44
+ describe('#setEventListenersLegacy', () => {
45
+ const message: PostMessageLegacy = {
46
+ message: 'transaction.complete',
47
+ data: {
48
+ action: 'transaction.complete'
49
+ }
50
+ }
51
+
52
+ it('selects the correct handler ', (done) => {
53
+ const document = window.document;
54
+ const iframe = document.createElement('iframe');
55
+ const handler = (data: any) => {
56
+ expect(data.action).toEqual('transaction.complete')
57
+ done();
58
+ };
59
+
60
+ document.body.appendChild(iframe);
61
+
62
+ const client = new PostMessageClient({
63
+ channel: 'sca'
64
+ })
65
+
66
+ client.setEventListenersLegacy({
67
+ 'transaction.complete': handler
68
+ })
69
+
70
+ window.postMessage(message, '*')
71
+ })
72
+ })
73
+ })
@@ -0,0 +1,125 @@
1
+ import { NODE_ENV } from '../shared/types'
2
+
3
+ interface PostMessage {
4
+ channel: string // appplepay, sca
5
+ subject: string // create payment, tokenize card, etc
6
+ data: unknown
7
+ }
8
+
9
+ interface PostMessageLegacy {
10
+ message: string // similar to subject above
11
+ data: {
12
+ [key: string]: string | boolean
13
+ }
14
+ }
15
+
16
+ interface PostMessageClientConfig {
17
+ channel: string
18
+ target?: HTMLIFrameElement
19
+ }
20
+
21
+ interface EventHandlers {
22
+ [key: string]: (data: any) => void
23
+ }
24
+
25
+ interface CreateCardTokenPostMessageData {
26
+ access_token: string,
27
+ card_holder: string,
28
+ card_number: string,
29
+ card_expiry: string,
30
+ cvv: string,
31
+ }
32
+
33
+ interface CreatePurchasePostMessageData {
34
+ access_token: string,
35
+ amount: number,
36
+ card_token: string,
37
+ currency: string,
38
+ extra?: { [key: string]: boolean | string },
39
+ hash: string,
40
+ invoice: string,
41
+ test?: boolean,
42
+ }
43
+
44
+ const domain = process.env.PAYNOW_BASE_URL
45
+
46
+ // NODE_ENV is introduced to PostMessageClient purely for the purpose of bypassing security
47
+ // check on the postMessage event between parent and child frames. Unforunately there is
48
+ // no workaround to override event origin due to lack of support in existing test framework.
49
+
50
+ class PostMessageClient {
51
+ private channel: string
52
+ private fzEnv: NODE_ENV | undefined
53
+ private target: HTMLIFrameElement
54
+
55
+ constructor(config: PostMessageClientConfig) {
56
+ this.channel = config.channel
57
+ this.fzEnv = process.env.NODE_ENV as NODE_ENV
58
+ this.target = config.target
59
+ }
60
+
61
+ // Handle event messages that conform to PostMessage format.
62
+ setEventListeners(handlers: EventHandlers): void {
63
+ window.addEventListener("message", async(event) => {
64
+
65
+ const validationEnabled = ![NODE_ENV.TEST, NODE_ENV.LOCALDEV].includes(this.fzEnv)
66
+ const isChannelValid = event.data.channel === this.channel
67
+ const isOriginValid = event.origin === domain
68
+
69
+ if (validationEnabled && (!isOriginValid || !isChannelValid)) {
70
+ console.log('[WARNING] unmatched event origin or incorrect message channel')
71
+ return
72
+ }
73
+
74
+ const message = event.data as PostMessage
75
+ const subject = message.subject
76
+ const handler = handlers[subject]
77
+
78
+ if (handler) {
79
+ handler(message.data)
80
+ } else {
81
+ // Do not throw error here as it is possible that the parent frame chooses
82
+ // not to handle certain messages posted from child frame.
83
+ console.log(`[WARNING] No handler registered for subject ${subject}, message ${JSON.stringify(message)}`)
84
+ }
85
+ })
86
+ }
87
+
88
+ // Handle event messages that do not conform to PostMessage format.
89
+ setEventListenersLegacy(handlers: EventHandlers):void {
90
+ window.addEventListener("message", (event) => {
91
+
92
+ const validationEnabled = ![NODE_ENV.TEST, NODE_ENV.LOCALDEV].includes(this.fzEnv)
93
+ const isOriginValid = event.origin === domain
94
+
95
+ if (validationEnabled && !isOriginValid) {
96
+ console.log('[WARNING] unmatched event origin or incorrect message channel')
97
+ return
98
+ }
99
+
100
+ const message = event.data as PostMessageLegacy
101
+ const handler = handlers[message.message]
102
+
103
+ if (handler) {
104
+ handler(message.data)
105
+ } else {
106
+ // Do not throw error here as it is possible that the parent frame chooses
107
+ // not to handle certain messages posted from child frame.
108
+ console.log(`[WARNING] no handler registered for message ${message.message}`)
109
+ }
110
+ })
111
+ }
112
+
113
+ send(message: PostMessage | PostMessageLegacy): void {
114
+ this.target.contentWindow.postMessage(message, domain)
115
+ }
116
+ }
117
+
118
+ export {
119
+ CreateCardTokenPostMessageData,
120
+ CreatePurchasePostMessageData,
121
+ PostMessage,
122
+ PostMessageLegacy,
123
+ PostMessageClient,
124
+ PostMessageClientConfig,
125
+ }