├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── Gemfile ├── LICENSE.txt ├── _config.yml ├── _data │ ├── buttons.yml │ ├── details.yml │ ├── params.yml │ └── response.yml ├── _includes │ ├── button.html │ ├── checkout.html │ └── fondy.svg ├── config.rb ├── docs │ ├── getting-started.md │ ├── index.md │ ├── installation.md │ ├── request-parameters.md │ └── test-payment-details.md ├── examples │ ├── button.html │ ├── checkout.html │ └── index.md ├── favicon.ico ├── index.html └── static │ ├── checkout.js │ ├── checkout.min.js │ └── checkout.min.js.map ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── checkout.js ├── core │ ├── api.js │ ├── class.js │ ├── component.js │ ├── config.js │ ├── connector.js │ ├── deferred.js │ ├── event.js │ ├── google │ │ └── pay.js │ ├── modal.js │ ├── model.js │ ├── module.js │ ├── payment │ │ ├── button.js │ │ ├── container.js │ │ ├── element.js │ │ └── request.js │ ├── response.js │ ├── template.js │ ├── utils.js │ └── widget │ │ ├── button.js │ │ ├── form.js │ │ └── index.js └── views │ ├── index.js │ └── template │ ├── acs.js │ └── acs_styles.js └── tests ├── index.html └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .wrangler 3 | .well-known 4 | node_modules 5 | dist 6 | showcase 7 | src/worker 8 | npm-debug.log 9 | wrangler.toml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Fondy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | IPSP JS SDK 3 |
4 | IPSP JS SDK 5 |
6 |

7 | 8 |

9 | 10 |

11 | 12 | 13 | 14 | 15 | 16 |

17 | 18 | 19 | ## Installation 20 | 21 | ### Node 22 | 23 | If you’re using [Npm](https://npmjs.com/) in your project, you can add `ipsp-js-sdk` dependency to `package.json` 24 | with following command: 25 | 26 | ```cmd 27 | npm i --save ipsp-js-sdk 28 | ``` 29 | 30 | or add dependency manually: 31 | 32 | ```json 33 | { 34 | "dependency": { 35 | "ipsp-js-sdk":"^2.0" 36 | } 37 | } 38 | ``` 39 | 40 | ### Bower 41 | 42 | If you’re using [Bower](https://bower.io/) in your project, you can run the following command: 43 | 44 | ```cmd 45 | bower install ipsp-js-sdk 46 | ``` 47 | 48 | ### Manual installation 49 | 50 | If you do not use NodeJS, you can download the 51 | [latest release](https://github.com/cloudipsp/ipsp-js-sdk/releases). 52 | Or clone from GitHub the latest developer version 53 | ```cmd 54 | git clone git@github.com:cloudipsp/ipsp-js-sdk.git 55 | ``` 56 | 57 | 58 | ## Quick start 59 | 60 | ```html 61 | 62 | ``` 63 | 64 | ## Basic template 65 | 66 | ```html 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 85 | 86 | 87 | ``` 88 | 89 | ## PaymentButton template (ApplePay/GooglePay) 90 | 91 | ```html 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 | 116 | 117 | 118 | ``` 119 | 120 | ## Parameters 121 | 122 | ### Host-to-host token 123 | 124 | 125 | ```json 126 | { 127 | "payment_system":"Supported payment systems: card, p24", 128 | "token":"host-to-host generated token", 129 | "card_number":"16/19-digits number", 130 | "expiry_date":"Supported formats: MM/YY, MM/YYYY, MMYY, MMYYYY", 131 | "cvv2":"3-digits number" 132 | } 133 | ``` 134 | 135 | Where token is value, returned in paymentgateway response from API enpoint /api/checkout/token (more details in API documentation: https://docs.fondy.eu/docs/page/3/ ) 136 | 137 | request example: 138 | 139 | ``` 140 | curl -i -X POST \ 141 | -H "Content-Type:application/json" \ 142 | -d \ 143 | '{ 144 | "request": { 145 | "server_callback_url": "http://myshop/callback/", 146 | "order_id": "TestOrder1", 147 | "currency": "USD", 148 | "merchant_id": 1396424, 149 | "order_desc": "Test payment", 150 | "amount": 1000 151 | } 152 | }' \ 153 | 'https://api.fondy.eu/api/checkout/token' 154 | ``` 155 | 156 | response example: 157 | ```json 158 | { 159 | "response": { 160 | "response_status": "success", 161 | "token": "7ddb3fbb03d60787b3972ef8d6fad0f97f7d2f86" 162 | } 163 | } 164 | ``` 165 | 166 | 167 | 168 | ### Client-side merchant ID 169 | 170 | ```json 171 | { 172 | "payment_system":"Supported payment systems: card, p24", 173 | "merchant_id":"1396424", 174 | "currency":"USD", 175 | "amount":"100.20", 176 | "card_number":"16/19-digits number", 177 | "expiry_date":"Supported formats: MM/YY, MM/YYYY, MMYY, MMYYYY", 178 | "cvv2":"3-digits number" 179 | } 180 | ``` 181 | 182 | optional merchant parameters: 183 | 184 | 185 | ```json 186 | { 187 | "email":"customer email address", 188 | "phone":"customer phone number" 189 | } 190 | ``` 191 | ## JS SDK usage example 192 | 193 | https://jsfiddle.net/fondyeu/jdc9o5cx/ 194 | 195 | 196 | ## License 197 | 198 | [MIT](https://github.com/cloudipsp/ipsp-js-sdk/blob/HEAD/LICENSE) 199 | 200 | ## Author 201 | 202 | [![Stepan Kosatyi](https://img.shields.io/badge/stepan-kosatyi-purple.svg)](https://kosatyi.com/) 203 | [![Fondy.io](https://img.shields.io/badge/fondy-io-lightgreen.svg)](https://fondy.io/) 204 | 205 | 206 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .bundle 3 | .sass-cache 4 | Gemfile.lock 5 | _site 6 | *.gem 7 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'github-pages', group: :jekyll_plugins 3 | gem 'kramdown' -------------------------------------------------------------------------------- /docs/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Parker Moore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: kosatyi/sjt 2 | 3 | navigation: true 4 | projects: true 5 | 6 | exclude: 7 | - .idea 8 | - node_modules 9 | - package.json 10 | - package-lock.json 11 | - Gemfile 12 | - Gemfil.lock 13 | - gulpfile.js 14 | 15 | plugins: 16 | - jekyll-seo-tag 17 | - jekyll-sitemap 18 | - jekyll-feed 19 | - jekyll-paginate 20 | - jekyll-redirect-from 21 | 22 | baseurl: /ipsp-js-sdk 23 | 24 | brand: IPSP JS SDK 25 | title: IPSP JS SDK 26 | description: Fondy Payment System 27 | logo: fondy.svg 28 | 29 | author: 30 | name: Fondy Developer Team 31 | url: https://fondy.io/ 32 | 33 | toolbar: 34 | - 35 | name: Docs 36 | link: /docs/ 37 | - 38 | name: Examples 39 | link: /examples/ 40 | 41 | 42 | defaults: 43 | - 44 | scope: 45 | path: "" 46 | type: "pages" 47 | values: 48 | layout: default 49 | -------------------------------------------------------------------------------- /docs/_data/buttons.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | title: Black Button 3 | summary: 4 | style: 5 | type: long 6 | color: black 7 | height: 46 8 | mode: default 9 | 10 | #white: 11 | # title: White Button 12 | # summary: 13 | # style: 14 | # type: long 15 | # color: white 16 | # height: 46 17 | # mode: default 18 | 19 | #buy: 20 | # title: Buy Button 21 | # summary: 22 | # style: 23 | # type: long 24 | # color: black 25 | # height: 46 26 | # mode: buy 27 | # 28 | #book: 29 | # title: Book Button 30 | # summary: 31 | # style: 32 | # type: long 33 | # color: black 34 | # height: 46 35 | # mode: book 36 | # 37 | #checkout: 38 | # title: Checkout Button 39 | # summary: 40 | # style: 41 | # type: long 42 | # color: black 43 | # height: 46 44 | # mode: checkout 45 | # 46 | #donate: 47 | # title: Donate Button 48 | # summary: 49 | # style: 50 | # type: long 51 | # color: black 52 | # height: 46 53 | # mode: donate 54 | # 55 | #order: 56 | # title: Order Button 57 | # summary: 58 | # style: 59 | # type: long 60 | # color: black 61 | # height: 46 62 | # mode: order 63 | # 64 | #pay: 65 | # title: Pay Button 66 | # summary: 67 | # style: 68 | # type: long 69 | # color: black 70 | # height: 46 71 | # mode: pay 72 | # 73 | #subscribe: 74 | # title: Subscribe Button 75 | # summary: 76 | # style: 77 | # type: long 78 | # color: black 79 | # height: 46 80 | # mode: subscribe 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /docs/_data/details.yml: -------------------------------------------------------------------------------- 1 | params: 2 | - 3 | name: merchant id 4 | value: 1396424 5 | - 6 | name: transaction password 7 | value: test 8 | - 9 | name: currency 10 | value: EUR, USD, GBP, RUB, UAH 11 | 12 | cards: 13 | - 14 | number: 4444555566661111 15 | date: any 16 | cvv2: any 17 | 3ds: yes 18 | response: approve 19 | - 20 | number: 4444111166665555 21 | date: any 22 | cvv2: any 23 | 3ds: yes 24 | response: decline 25 | - 26 | number: 4444555511116666 27 | date: any 28 | cvv2: any 29 | 3ds: no 30 | response: approve 31 | - 32 | number: 4444111155556666 33 | date: any 34 | cvv2: any 35 | 3ds: no 36 | response: decline 37 | -------------------------------------------------------------------------------- /docs/_data/params.yml: -------------------------------------------------------------------------------- 1 | card_number: 2 | type: integer(19) 3 | descr: Visa/MC card number mandatory 4 | sample: 5 | mandatory: true 6 | 7 | cvv2: 8 | type: integer(4) 9 | descr: Card CVV2/CVC2 code 10 | sample: 11 | mandatory: true 12 | 13 | expiry_date: 14 | type: integer(4) 15 | descr: Card expiry date in format MMYY 16 | mandatory: true 17 | 18 | client_ip: 19 | type: integer(15) 20 | descr: Client IP 21 | mandatory: true 22 | 23 | comment: 24 | type: string(1024) 25 | descr: Merchant comment on reversal reason UTF-8 26 | sample: Customer returned product back 27 | mandatory: true 28 | 29 | order_id: 30 | type: string(1024) 31 | descr: Order ID which is generated by merchant. 32 | sample: ID1234 33 | mandatory: true 34 | 35 | merchant_id: 36 | type: integer(12) 37 | descr: Merchant unique ID. Generated by FONDY during merchant registration. 38 | sample: 1 39 | mandatory: true 40 | 41 | order_desc: 42 | type: string(1024) 43 | descr: Order description. Generated by merchant in UTF-8. 44 | sample: Hotel booking №1234 Antalia Resort 45 | mandatory: true 46 | 47 | signature: 48 | type: string(40) 49 | descr: Order signature. Required to verify merchant request consistency and authenticity. 50 | Signature generation algorithm please see at Signature generation for request and response 51 | sample: 1 52 | mandatory: true 53 | 54 | amount: 55 | type: integer(12) 56 | descr: Order amount in cents without separator 57 | sample: 1020 (means 10 euros and 20 cents) 58 | mandatory: true 59 | 60 | currency: 61 | type: string(3) 62 | descr: Order currency. Supported values (UAH, RUB, USD, EUR, GBP) 63 | sample: EUR 64 | mandatory: true 65 | 66 | date_from: 67 | type: string(10) 68 | descr: Settlement date in fromat DD.MM.YYYY 69 | sample: 15.04.2014 70 | mandatory: true 71 | 72 | date_to: 73 | type: string(10) 74 | descr: Settlement date in fromat DD.MM.YYYY 75 | sample: 16.04.2014 76 | mandatory: true 77 | 78 | version: 79 | type: string(10) 80 | descr: Protocol version. 81 | default: 1.0 82 | sample: 1.0 83 | 84 | response_url: 85 | type: string(2048) 86 | descr: Merchant site URL, where customer will be redirected after payment completion. 87 | default: https://api.fondy.eu/checkout/responsepage 88 | sample: http://example.com/response 89 | 90 | server_callback_url: 91 | type: string(2048) 92 | descr: Merchant site URL, where host-to-host callback will be send after payment completion. 93 | sample: http://example.com/callback 94 | 95 | payment_systems: 96 | type: string(1024) 97 | descr: Payment systems which can be used for payment by customer at FONDY payment page. Systems must be separated by a comma or semicolon. 98 | default: is set from merchant settings in FONDY merchant portal 99 | sample: sepa,card,direct_debit 100 | 101 | default_payment_system: 102 | type: string(25) 103 | descr: Payment system which will be shown to customer at FONDY payment page first. 104 | sample: card 105 | 106 | lifetime: 107 | type: integer(6) 108 | descr: Order lifetime in seconds. After this time, the order will be given the status of 'expired' if the client has not paid it 109 | default: 36000 110 | sample: 600 111 | 112 | 113 | merchant_data: 114 | type: string(2048) 115 | descr: Any arbitrary set of data that a merchant wants to get back in the response to response_url or/and server_callback_url, and also in reports 116 | sample: 117 | 118 | preauth: 119 | type: string(1) 120 | descr: Parameter supported only for Visa/MasterCard payment method 121 | N — amount is debited from the customer's card immediately and settled to the merchant account, in accordance with the rules of settlements. 122 | Y — amount holded on the customer card and not charged until the merchant sends a 'capture' request to confirm 123 | 124 | default: N 125 | sample: Y 126 | 127 | sender_email: 128 | type: string(254) 129 | descr: Customer email 130 | sample: customer@example.com 131 | 132 | delayed: 133 | type: string(1) 134 | descr: Delayed order flag. 135 | Y — allows customer to pay the order during period sent by merchant in lifetime parameter. Merchant must expect several host-to-host callbacks and browser redirects at the same order. Customer will have possibility to try to pay the same order_id, if previous attempt failed 136 | N — after payment is declined order_id customer will be redirected to merchant site to recreate the order. In this case only one callback will be sent to server_callback_url 137 | default: Y 138 | sample: N 139 | 140 | lang: 141 | type: string(2) 142 | descr: Payment page language. Supported values (ru,uk,en,lv,fr,cs) 143 | sample: en 144 | 145 | product_id: 146 | type: string(1024) 147 | descr: Merchant product or service id 148 | sample: en 149 | 150 | required_rectoken: 151 | type: string(1) 152 | descr: Flag which indicates whether FONDY must return card token — token to access card funds without cardholder interaction 153 | default: N 154 | sample: Y 155 | 156 | verification: 157 | type: string(1) 158 | descr: If Y order will be automatically reversed by FONDY after successful approval 159 | default: N 160 | sample: Y 161 | 162 | verification_type: 163 | type: string(25) 164 | descr: amount - amount submitted from merchant will be hold on card 165 | code - amount submitted from merchant will be hold on card. Also cardholder have to enter 4-characters code to pass verification 166 | default: amount 167 | sample: code 168 | 169 | rectoken: 170 | type: string(40) 171 | descr: Card token — token to access card funds without cardholder interaction 172 | sample: 544d3f86237886b6404d8b000f6a7d71c45410b7 173 | 174 | receiver_rectoken: 175 | type: string(40) 176 | descr: Card token — token to credit card without transfering full card number 177 | sample: 544d3f86237886b6404d8b000f6a7d71c45410b7 178 | 179 | design_id: 180 | type: integer(6) 181 | descr: ID of design which is set in merchant portal 182 | sample: 123 183 | 184 | subscription: 185 | type: string(1) 186 | descr: Y - enable scheduled payments 187 | N - by default, disable scheduled payments 188 | default: N 189 | sample: Y 190 | 191 | subscription_callback_url: 192 | type: string(2048) 193 | descr: Merchant site URL, where host-to-host callback will be send after scheduled payment completion 194 | sample: http://example.com/subscription_callback 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /docs/_data/response.yml: -------------------------------------------------------------------------------- 1 | order_id: 2 | type: string(1024) 3 | descr: Order ID which is generated by merchant. 4 | 5 | merchant_id: 6 | type: integer(12) 7 | descr: Merchant unique ID. Generated by FONDY during merchant registration. 8 | sample: 9 | 10 | amount: 11 | type: integer(12) 12 | descr: Order amount in cents without separator 13 | sample: 1020 (EUR) (means 10 euros and 20 cents) 14 | 15 | currency: 16 | type: string(3) 17 | descr: Order currency. Supported values ( UAH, RUB, USD, EUR, GBP) 18 | sample: EUR 19 | 20 | order_status: 21 | type: string(50) 22 | descr: Order processing status. Can contain next values > 23 | created — order has been created, but the customer has not entered payment details yet; merchant must continue to request the status of the order 24 | processing — order is still in processing by payment gateway; merchant must continue to request the status of the order 25 | declined — order is declined by FONDY payment gateway or by bank or by external payment system 26 | approved — order completed successfully, funds are hold on the payer's account and soon will be credited of the merchant; merchant can provide the service or ship goods 27 | expired — order lifetime expired. 28 | reversed — previously approved transaction was fully or partially reversed. In this case parameter reversal_amount will be > 0 29 | 30 | response_status: 31 | type: string(50) 32 | descr: Request processing status. If parameters sent by merchant did not pass validation then failure, else success 33 | 34 | signature: 35 | type: string(40) 36 | descr: Order signature. Required to verify merchant request consistency and authenticity. Signature generation algorithm please see at Signature generation for request and response 37 | sample: 1773cf135bd89656131134b98637894dad42f808 38 | 39 | tran_type: 40 | type: string(50) 41 | descr: Supported values > (purchase, reverse) 42 | 43 | sender_cell_phone: 44 | type: string(16) 45 | descr: Customer mobile phone number 46 | 47 | sender_account: 48 | type: string(50) 49 | descr: Customer payment account 50 | 51 | masked_card: 52 | type: string(19) 53 | descr: Masked card number 54 | sample: 444444XXXXXX5555 55 | 56 | card_bin: 57 | type: integer(6) 58 | descr: Card bin — usually first 6 digits 59 | sample: 444444 60 | 61 | card_type: 62 | type: string(50) 63 | descr: Supported values > (VISA, MasterCard) 64 | 65 | rrn: 66 | type: string(50) 67 | descr: Commonly not unique transaction ID returned by bank. 68 | 69 | approval_code: 70 | type: string(6) 71 | descr: Commonly not unique authorization code returned by bank. 72 | 73 | response_code: 74 | type: integer(4) 75 | descr: Order decline response code. Possible codes see in Response codes 76 | 77 | response_description: 78 | type: string(1024) 79 | descr: Order response code description, see Response codes 80 | 81 | reversal_amount: 82 | type: integer(12) 83 | descr: Total amount of all reversals for current order 84 | 85 | settlement_amount: 86 | type: integer(12) 87 | descr: Settlement amount for current order 88 | 89 | settlement_amount: 90 | type: integer(12) 91 | descr: Settlement amount for current order 92 | 93 | settlement_currency: 94 | type: string(3) 95 | descr: Currency of order settlement 96 | 97 | order_time: 98 | type: string(19) 99 | descr: Order creation date DD.MM.YYYY hh:mm:ss 100 | sample: 21.12.2014 11:21:30 101 | 102 | settlement_date: 103 | type: string(10) 104 | descr: Settlement date in format DD.MM.YYYY 105 | sample: 21.12.2014 106 | 107 | eci: 108 | type: integer(2) 109 | descr: Ecommerce Indicator - parameter specifies whether 3DSecure authentication was performed or not. Supported values > 110 | 5 — full 3DSecure authentication performed 111 | 6 — merchant supports 3DSecure, but issuing bunk does not 112 | 7 — neither merchant nor issuing bank supports 3DSecure 113 | 114 | fee: 115 | type: integer(12) 116 | descr: Fee charged by FONDY 117 | 118 | payment_system: 119 | type: string(50) 120 | descr: Payment system which was used for payment. Supported payment systems list see Supported payment systems 121 | sample: Qiwi 122 | 123 | sender_email: 124 | type: string(254) 125 | descr: Customer email 126 | 127 | payment_id: 128 | type: integer(19) 129 | descr: Unique paymnet ID generated by FONDY payment gateway 130 | 131 | actual_amount: 132 | type: integer(12) 133 | descr: Actual amount after all reversals. 134 | 135 | actual_currency: 136 | type: string(3) 137 | descr: Actual currency 138 | 139 | product_id: 140 | type: string(1024) 141 | descr: Merchant product or service ID 142 | 143 | merchant_data: 144 | type: string(2048) 145 | descr: Any arbitrary set of data that a merchant sends in request 146 | 147 | verification_status: 148 | type: string(50) 149 | descr: Code verification result 150 | Supported values 151 | verified — card successfully verified with code 152 | incorrect — incorrect code entered but limit not exceeded yet 153 | failed — allowed number of invalid attempts to enter code exceeded 154 | created — verification code created but not entered yet 155 | 156 | rectoken: 157 | type: string(40) 158 | descr: Flag which indicates whether FONDY must return card token — token to access card funds without cardholder interaction 159 | sample: da39a3ee5e6b4b0d3255bfef95601890afd80709 160 | 161 | rectoken_lifetime: 162 | type: string(19) 163 | descr: Token lifetime in format DD.MM.YYYY hh:mm:ss 164 | sample: 01.01.2018 00:00:00 165 | 166 | checkout_url: 167 | type: string(2048) 168 | descr: FONDY payment page URL where merchant site must redirect customer to enter payment details 169 | sample: https://api.fondy.eu/checkout?token=e0a5d4f331806d1e2feb80353b4c44bf6751fc8c 170 | 171 | payment_id: 172 | type: integer(19) 173 | descr: Unique payment ID generated by FONDY payment gateway 174 | 175 | error_code: 176 | type: integer(4) 177 | descr: Response decline code. Supported values see Response codes 178 | 179 | error_message: 180 | type: string(1024) 181 | descr: Response code description. See Response codes 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /docs/_includes/button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32 | 33 | 74 | 75 | {% assign examples = site.data.buttons %} 76 | {% for item in examples %} 77 | {% assign name = item[0] %} 78 | {% assign options = item[1] %} 79 |
80 |

{{ options.title }}

81 | {% highlight json %}{{ options.style | jsonify }}{% endhighlight %} 82 |
83 | 84 |
85 | {% endfor %} 86 | -------------------------------------------------------------------------------- /docs/_includes/checkout.html: -------------------------------------------------------------------------------- 1 | 85 | 86 | 87 | 88 |
89 | 90 | 91 | 92 | 93 | 94 |
95 |
96 |
97 |
98 | Card Number: 99 |
100 |
101 | 102 |
103 |
104 |
105 |
106 | Expiry Date: 107 |
108 |
109 | CVV2: 110 |
111 |
112 | 113 |
114 |
115 | 116 |
117 |
118 | 119 |
120 |
121 |
122 |
123 | 124 |
125 |
126 |
127 |
128 | 129 | 140 | -------------------------------------------------------------------------------- /docs/_includes/fondy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | -------------------------------------------------------------------------------- /docs/config.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudipsp/ipsp-js-sdk/59e69bfc7dd6cde4dc15af31f6adbd314da6fe91/docs/config.rb -------------------------------------------------------------------------------- /docs/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | headline: Getting Started 4 | sort: 2 5 | --- 6 | 7 | ## Quick start 8 | 9 | ```html 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 28 | 29 | 30 | ``` 31 | 32 | ## Parameters 33 | 34 | ### Host-to-host token 35 | 36 | For server-side token generation see [ipsp php sdk](https://ipsp-php.com/) project. 37 | 38 | ```json 39 | { 40 | "payment_system":"Supported payment systems: card, p24", 41 | "token":"host-to-host generated token", 42 | "card_number":"16/19-digits number", 43 | "expiry_date":"Supported formats: MM/YY, MM/YYYY, MMYY, MMYYYY", 44 | "cvv2":"3-digits number" 45 | } 46 | ``` 47 | 48 | ### Client-side merchant ID 49 | 50 | 51 | ```json 52 | { 53 | "payment_system":"Supported payment systems: card, p24", 54 | "merchant_id":"1396424", 55 | "currency":"USD", 56 | "amount":"100.20", 57 | "card_number":"16/19-digits number", 58 | "expiry_date":"Supported formats: MM/YY, MM/YYYY, MMYY, MMYYYY", 59 | "cvv2":"3-digits number" 60 | } 61 | ``` 62 | 63 | optional merchant parameters: 64 | 65 | 66 | ```json 67 | { 68 | "email":"customer email address", 69 | "phone":"customer phone number" 70 | } 71 | ``` -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sort: 1 3 | --- 4 | 5 | # Documentation 6 | 7 | Download and installation, API methods description, frequently asked questions , organizing and releasing your source project. 8 | 9 | ## Initialize Api Component 10 | 11 | ### Singleton usage 12 | 13 | ```javascript 14 | var checkout = $checkout('Api'); 15 | ``` 16 | 17 | Use **$checkout('Api')** instance in inside nested scope 18 | 19 | ```javascript 20 | var result = (function(instance){ 21 | return $checkout('Api') === instance; // ==> their instances are equal 22 | })(checkout); 23 | ``` 24 | 25 | ### New Instance usage 26 | 27 | Create new iframe connector for every Checkout.Api instance 28 | 29 | ```javascript 30 | var checkout = $checkout.get('Api'); 31 | ``` 32 | 33 | ## Send Request 34 | 35 | ### Method **.request()** 36 | 37 | ```javascript 38 | $checkout('Api').request('api.checkout.form',{ params }); // return Defer object 39 | ``` 40 | 41 | ### Basic params example 42 | 43 | #### Token param (host-to-host) 44 | 45 | ```javascript 46 | $checkout('Api').request('api.checkout.form',{ 47 | token:'host-to-host-token', 48 | card_number:'4444555566661111', 49 | expire_date:'12/30', 50 | cvv2:'111' 51 | }); 52 | ``` 53 | 54 | #### Generation Params 55 | 56 | ```javascript 57 | $checkout('Api').request('api.checkout.form',{ 58 | merchant_id:123456, 59 | amount:250, 60 | currency:'USD', 61 | card_number:'4444555566661111', 62 | expire_date:'12/30', 63 | cvv2:'111' 64 | }); 65 | ``` 66 | 67 | > [Additional Generation Params](request-parameters.md) 68 | 69 | #### Custom user field object 70 | 71 | Add user field object to property **custom<Object>** with field name as object key. 72 | Mandatory properties for child object is **label** and **value**. 73 | 74 | 75 | ```javascript 76 | $checkout('Api').request('api.checkout.form',{ 77 | merchant_id:123456, 78 | amount:250, 79 | currency:'USD', 80 | card_number:'4444555566661111', 81 | expire_date:'12/30', 82 | cvv2:'111', 83 | custom: { 84 | customer: { 85 | label: 'Customer', 86 | value: 'John Doe' 87 | }, 88 | user_id: { 89 | label: 'User ID', 90 | value: 1234567890 91 | } 92 | } 93 | }); 94 | ``` 95 | 96 | You can use form nested name values to serve correct structure for custom field object as well. 97 | 98 | Example: 99 | ```html 100 | 101 | 102 | 103 | 104 | 105 | 106 | ``` 107 | 108 | ## Response model 109 | 110 | ### Methods 111 | 112 | #### attr('property') 113 | 114 | #### sendResponse() 115 | 116 | #### submitToMerchant() 117 | 118 | #### waitOn3dsDecline() 119 | 120 | #### submit3dsForm() 121 | 122 | #### needVerifyCode() 123 | 124 | ### Properties 125 | 126 | #### model.attr('info') 127 | 128 | #### model.attr('order') 129 | 130 | 131 | ## Handle Response 132 | 133 | ### 3d secure modal window 134 | 135 | ![](https://i.imgur.com/cneeQpL.jpg) 136 | {:.text-center} 137 | 138 | ### 3d secure timeout request 139 | 140 | ```javascript 141 | $checkout('Api').request('api.checkout.form',{ params }).notify(function(model){ 142 | if(model.waitOn3dsDecline()){ 143 | // show timer for next 3ds modal window 144 | setTimeout(function(){ 145 | model.submit3dsForm(); 146 | },model.waitOn3dsDecline()); 147 | } 148 | }); 149 | ``` 150 | 151 | ![](https://i.imgur.com/cneeQpL.jpg) 152 | {:.text-center} 153 | 154 | ### Submit to result page 155 | 156 | Use method **submitToMerchant()** if **response_url** parameter is specified to redirect user on merchant page with 157 | order data object. 158 | 159 | ```javascript 160 | $checkout('Api').request('api.checkout.form',{ 161 | ... 162 | response_url:'https://example.com/' 163 | ... 164 | }).done(function(model){ 165 | model.submitToMerchant(); 166 | }); 167 | ``` 168 | 169 | ### Handle gateway errors 170 | 171 | ```javascript 172 | $checkout('Api').request('api.checkout.form',{ params }).fail(function(model){ 173 | model.attr('error.code'); 174 | model.attr('error.message'); 175 | }); 176 | ``` 177 | 178 | 181 | -------------------------------------------------------------------------------- /docs/docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | headline: Installation 4 | sort: 1 5 | --- 6 | 7 | ## Node 8 | 9 | If you’re using [Npm](https://npmjs.com/) in your project, you can add `ipsp-js-sdk` dependency to `package.json` 10 | with following command: 11 | 12 | ```cmd 13 | npm i --save ipsp-js-sdk 14 | ``` 15 | 16 | or add dependency manually: 17 | 18 | ```json 19 | { 20 | "dependency": { 21 | "ipsp-js-sdk":"^1.0" 22 | } 23 | } 24 | ``` 25 | 26 | ## Bower 27 | 28 | If you’re using [Bower](https://bower.io/) in your project, you can run the following command: 29 | 30 | ```cmd 31 | bower install ipsp-js-sdk 32 | ``` 33 | 34 | ## Manual installation 35 | 36 | If you do not use NodeJS, you can download the 37 | [latest release](https://github.com/cloudipsp/ipsp-js-sdk/releases). 38 | Or clone from GitHub the latest developer version 39 | ```cmd 40 | git clone git@github.com:cloudipsp/ipsp-js-sdk.git 41 | ``` 42 | 43 | ## CDN 44 | 45 | ```html 46 | 47 | ``` -------------------------------------------------------------------------------- /docs/docs/request-parameters.md: -------------------------------------------------------------------------------- 1 | --- 2 | sort: 5 3 | --- 4 | 5 | # Request Parameters 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for row in site.data.params %} 23 | 24 | 25 | 26 | 27 | 28 | {% endfor %} 29 | 30 |
ParameterTypeDescription
{{ row[0] }}{{ row[1].type }}{{ row[1].descr }}
31 | 32 | -------------------------------------------------------------------------------- /docs/docs/test-payment-details.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Test Payment Details 3 | headline: Test Payment Details 4 | sort: 6 5 | --- 6 | 7 | ## Merchant parameters 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for detail in site.data.details.params %} 23 | 24 | 25 | 26 | 27 | {% endfor %} 28 | 29 |
ParameterValue
{{ detail.name }}{{ detail.value }}
30 | 31 | 32 | ## Test card numbers 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {% for card in site.data.paymentdetails.cards %} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {% endfor %} 59 | 60 |
Card numberExpiry dateCVV23DSecureResponse type
{{ card['number'] }}{{ card['date'] }}{{ card['cvv2'] }}{{ card['3ds'] }}{{ card['response'] }}
61 | -------------------------------------------------------------------------------- /docs/examples/button.html: -------------------------------------------------------------------------------- 1 | --- 2 | sort: 2 3 | title: Apple/Google Pay Button 4 | --- 5 | 6 |

Apple/Google Pay Button

7 | 8 | {% include button.html %} 9 | -------------------------------------------------------------------------------- /docs/examples/checkout.html: -------------------------------------------------------------------------------- 1 | --- 2 | sort: 1 3 | title: Checkout Form 4 | --- 5 | 6 |

Checkout Form

7 | 8 | {% include checkout.html %} 9 | -------------------------------------------------------------------------------- /docs/examples/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sort: 2 3 | --- 4 | 5 | # Examples 6 | 7 | 10 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudipsp/ipsp-js-sdk/59e69bfc7dd6cde4dc15af31f6adbd314da6fe91/docs/favicon.ico -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: IPSP JS SDK 3 | description: Start e-commerce on your website. 4 | --- 5 | 6 |

7 | 8 | 9 | 10 | 11 |

12 | 13 |

14 | 15 |

16 | 17 |

Project section

18 | 19 | 22 | -------------------------------------------------------------------------------- /docs/static/checkout.min.js: -------------------------------------------------------------------------------- 1 | !function(t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).$checkout=t()}(function(){return function i(o,s,r){function a(e,t){if(!s[e]){if(!o[e]){var n="function"==typeof require&&require;if(!t&&n)return n(e,!0);if(c)return c(e,!0);throw(n=new Error("Cannot find module '"+e+"'")).code="MODULE_NOT_FOUND",n}n=s[e]={exports:{}},o[e][0].call(n.exports,function(t){return a(o[e][1][t]||t)},n,n.exports,i,o,s,r)}return s[e].exports}for(var c="function"==typeof require&&require,t=0;t/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g},h=/(.)^/,u={"'":"'","\\":"\\","\r":"r","\n":"n","\t":"t","\u2028":"u2028","\u2029":"u2029"},d=/\\|'|\r|\n|\t|\u2028|\u2029/g,r={"&":"&","<":"<",">":">",'"':""","'":"'"},a=new RegExp("[&<>\"']","g"),l=0,t=s.extend({utils:t,init:function(t){this.name=t,this.view={},this.output()},output:function(){this.view.source=o[this.name],this.view.output=function(s){var e,t=new RegExp([(c.escape||h).source,(c.interpolate||h).source,(c.evaluate||h).source].join("|")+"|$","g"),r=0,a="__p+='";s.replace(t,function(t,e,n,i,o){return a+=s.slice(r,o).replace(d,function(t){return"\\"+u[t]}),e&&(a+="'+\n((__t=("+e+"))==null?'':escapeExpr(__t))+\n'"),n&&(a+="'+\n((__t=("+n+"))==null?'':__t)+\n'"),i&&(a+="';\n"+i+"\n__p+='"),r=o+t.length,t}),a+="';\n",a="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+(a=!c.variable?"with(obj||{}){\n"+a+"}\n":a)+"return __p;\n//# sourceURL=/tmpl/source["+l+++"]";try{e=new Function(c.variable||"obj","escapeExpr",a)}catch(t){throw t.source=a,t}t=function(t){return e.call(this,t,i)};return t.source="function("+(c.variable||"obj")+"){\n"+a+"}",t}(this.view.source)},render:function(t){return this.data=t,this.view.output.call(this,this)},include:function(t,e){return this.instance(t).render(this.utils.extend(this.data,e))}});e.exports=t},{"../views":22,"./class":3,"./utils":18}],18:[function(t,e,n){var s={getType:function(t){return{}.toString.call(t).match(/\s([a-zA-Z]+)/)[1].toLowerCase()},isObject:function(t){return"object"===this.getType(t)},isPlainObject:function(t){return!!t&&"object"==typeof t&&t.constructor===Object},isFunction:function(t){return"function"===this.getType(t)},isRegexp:function(t){return"regexp"===this.getType(t)},isArguments:function(t){return"arguments"===this.getType(t)},isError:function(t){return"error"===this.getType(t)},isArray:function(t){return"array"===this.getType(t)},isDate:function(t){return"date"===this.getType(t)},isString:function(t){return"string"===this.getType(t)},isNumber:function(t){return"number"===this.getType(t)},isElement:function(t){return t&&1===t.nodeType},toArray:function(t){return[].slice.call(t)},querySelectorAll:function(t,e){return this.toArray((e||document).querySelectorAll(t))},querySelector:function(t,e){return(e||document).querySelector(t)},hasProp:function(t,e){return t&&t.hasOwnProperty(e)},forEach:function(t,e,n){for(var i in t)this.hasProp(t,i)&&e.call(n||null,t[i],i)},map:function(t,e,n){var i,o,s=[];for(i in t)this.hasProp(t,i)&&void 0!==(o=e.call(n||null,t[i],i))&&(s[i]=o);return s},cleanObject:function(t){for(var e in t)this.hasProp(t,e)&&(0===t[e].length?(this.isArray(t)&&t.splice(e,1),this.isPlainObject(t)&&delete t[e]):this.isPlainObject(t[e])&&this.cleanObject(t[e]));return t},param:function(t){function e(t,e){e=null==(e="function"==typeof e?e():e)?"":e,n[n.length]=encodeURIComponent(t)+"="+encodeURIComponent(e)}var n=[],o=function(i,t){return i?s.isArray(t)?s.forEach(t,function(t,e){var n;o(i+"["+(n=e,"object"==typeof(e=t)&&e?n:"")+"]",t)}):s.isObject(t)?s.forEach(t,function(t,e){o(i+"["+e+"]",t)}):e(i,t):s.isArray(t)?s.forEach(t,function(t){e(t.name,t.value)}):s.forEach(t,function(t,e){o(e,t)}),n};return o("",t).join("&")},removeElement:function(t){t.parentNode.removeChild(t)},createElement:function(t){return document.createElement(t)},getStyle:function(t,e,n){return((n=window.getComputedStyle)?n(t):t.currentStyle)[e.replace(/-(\w)/gi,function(t,e){return e.toUpperCase()})]},extend:function(n){return this.forEach([].slice.call(arguments,1),function(t){null!==t&&this.forEach(t,function(t,e){this.isPlainObject(t)?n[e]=this.extend(n[e]||{},t):n[e]=t},this)},this),n},uuid:function(){for(var t=0,e="";t++<36;)e+=51*t&52?(15^t?8^Math.random()*(20^t?16:4):4).toString(16):"-";return e},getPath:function(t){var e=t.split("."),t=e.shift(),n=null;return this.hasProp(window,t)&&(n=window[t],this.forEach(e,function(t){n=this.hasProp(n,t)?n[t]:null},this)),n},stringFormat:function(t,n){return(t||"").replace(/{(.+?)}/g,function(t,e){return n[e]||t})}};e.exports=s},{}],19:[function(t,e,n){t=t("./index").extend({attributes:{},initElement:function(t){this.utils.isPlainObject(this.params.attributes)&&this.utils.extend(this.attributes,this.params.attributes),this.addSelectorEvent(t,"click","sendRequest")},getRequestParams:function(t){return this.utils.extend({},this.params.options,this.getElementData(t))},getElementData:function(n){var i={};return this.utils.forEach(this.attributes,function(t,e){n.hasAttribute(e)&&(i[t]=n.getAttribute(e))}),i}});e.exports=t},{"./index":21}],20:[function(t,e,n){var i=t("../module"),t=t("./index"),o=i.extend({init:function(t){this.setFormElement(t)},setFormElement:function(t){this.utils.isElement(t)&&(this.form=t)},getData:function(t){var e=this.deparam(this.serializeArray());return!0===t?this.utils.cleanObject(e):e},serializeArray:function(){var t=this.utils.toArray(this.form.elements);return this.utils.map(t,function(t){if(!t.disabled&&""!==t.name&&(!t.type.match("checkbox|radio")||t.checked))return{name:t.name,value:t.value}})},serializeAndEncode:function(){return this.utils.map(this.serializeArray(),function(t){return[t.name,encodeURIComponent(t.value)].join("=")}).join("&")},deparam:function(t){var e,r={},n=/[^\[\]]+|\[\]$/g;for(e in t)t.hasOwnProperty(e)&&function(t,e){for(var n=r,i=t.pop(),o=t.length,s=0;s\n
\n
\n
\n \n
\n <%-data.messages.modalHeader%>\n <%-data.messages.modalLinkLabel%>\n
\n
\n
\n \n
\n
\n
\n\n',e.exports["styles.ejs"]='\n',e.exports["trustly.ejs"]='<%=include(\'styles.ejs\')%>\n
\n
\n
\n \n
\n <%-data.messages.modalHeader%>\n <%-data.messages.modalLinkLabel%>\n
\n
\n
\n \n
\n
\n
\n'},{}]},{},[1])(1)}); 2 | //# sourceMappingURL=checkout.min.js.map 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ipsp-js-sdk", 3 | "title": "Fondy Checkout SDK", 4 | "description": "Fondy payment service provider javascript sdk", 5 | "version": "2.0.12", 6 | "license": "MIT", 7 | "homepage": "https://fondy.eu", 8 | "main": "src/checkout.js", 9 | "unpkg": "dist/checkout.min.js", 10 | "browser": "dist/checkout.js", 11 | "author": { 12 | "name": "CloudIPSP JS Team", 13 | "url": "https://github.com/orgs/cloudipsp/teams/js-team" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/cloudipsp/ipsp-js-sdk.git" 18 | }, 19 | "scripts": { 20 | "dev": "npm-run-all --parallel dev:wrangler dev:rollup", 21 | "dev:wrangler": "wrangler dev", 22 | "dev:rollup": "./node_modules/.bin/rollup -c -w --bundleConfigAsCjs", 23 | "build": "npm install && ./node_modules/.bin/rollup -c --bundleConfigAsCjs", 24 | "prepublishOnly": "npm install && ./node_modules/.bin/rollup -c --bundleConfigAsCjs", 25 | "version": "npm run build", 26 | "postversion": "git push && git push --tags" 27 | }, 28 | "files": [ 29 | "dist", 30 | "src" 31 | ], 32 | "devDependencies": { 33 | "@babel/preset-env": "^7.22.4", 34 | "@rollup/plugin-babel": "^6.0.3", 35 | "@rollup/plugin-commonjs": "^24.0.1", 36 | "@rollup/plugin-json": "^6.0.0", 37 | "@rollup/plugin-node-resolve": "^15.0.1", 38 | "@rollup/plugin-terser": "^0.4.0", 39 | "hono": "^3.2.3", 40 | "npm-run-all": "^4.1.5", 41 | "qunit": "^2.14.1", 42 | "rollup": "^3.23.0", 43 | "rollup-plugin-html": "^0.2.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel' 2 | import resolve from '@rollup/plugin-node-resolve' 3 | import commonjs from '@rollup/plugin-commonjs' 4 | import terser from '@rollup/plugin-terser' 5 | import json from '@rollup/plugin-json'; 6 | import pkg from './package.json' 7 | 8 | export default { 9 | input: pkg.main, 10 | output: [ 11 | { 12 | file: 'dist/checkout.js', 13 | format: 'umd', 14 | name: '$checkout' 15 | }, 16 | { 17 | file: 'dist/checkout.min.js', 18 | format: 'umd', 19 | sourcemap: true, 20 | name: '$checkout', 21 | plugins:[ 22 | terser() 23 | ] 24 | }, 25 | ], 26 | plugins: [ 27 | commonjs(), 28 | resolve({}), 29 | json(), 30 | babel({ 31 | presets: ['@babel/preset-env'], 32 | babelHelpers: 'bundled' 33 | }) 34 | ] 35 | } -------------------------------------------------------------------------------- /src/checkout.js: -------------------------------------------------------------------------------- 1 | const {Component} = require('./core/component'); 2 | const Utils = require('./core/utils'); 3 | const Config = require('./core/config'); 4 | const {Api} = require('./core/api'); 5 | const {Module} = require('./core/module'); 6 | const {Connector} = require('./core/connector'); 7 | const {Response} = require('./core/response'); 8 | const {PaymentButton} = require('./core/payment/button'); 9 | const {PaymentRequestApi} = require('./core/payment/request'); 10 | const {PaymentElement} = require('./core/payment/element'); 11 | const {PaymentContainer} = require('./core/payment/container'); 12 | const {WidgetForm} = require('./core/widget/form'); 13 | const {WidgetButton} = require('./core/widget/button'); 14 | 15 | Component.add('Api', Api); 16 | Component.add('Connector', Connector); 17 | Component.add('PaymentButton', PaymentButton); 18 | Component.add('PaymentRequestApi', PaymentRequestApi); 19 | Component.add('PaymentElement', PaymentElement); 20 | Component.add('FormWidget', WidgetForm); 21 | Component.add('ButtonWidget', WidgetButton); 22 | 23 | Component.Utils = exports.Utils = Utils 24 | Component.Config = exports.Config = Config 25 | Component.Api = exports.Api = Api; 26 | Component.Module = exports.Module = Module; 27 | Component.Connector = exports.Connector = Connector; 28 | Component.PaymentRequestApi = exports.PaymentRequestApi = PaymentRequestApi; 29 | Component.PaymentContainer = exports.PaymentContainer = PaymentContainer; 30 | Component.PaymentElement = exports.PaymentElement = PaymentElement; 31 | Component.PaymentButton = exports.PaymentButton = PaymentButton; 32 | Component.Response = exports.Response = Response; 33 | 34 | module.exports = Component; -------------------------------------------------------------------------------- /src/core/api.js: -------------------------------------------------------------------------------- 1 | const { Deferred } = require('./deferred') 2 | const { Module } = require('./module') 3 | const { Connector } = require('./connector') 4 | const { Modal } = require('./modal') 5 | const { Response } = require('./response') 6 | const { ApiFrameCss, ApiOrigin, ApiEndpoint } = require('./config') 7 | 8 | exports.Api = Module.extend({ 9 | defaults: { 10 | origin: ApiOrigin, 11 | endpoint: ApiEndpoint, 12 | messages: { 13 | modalHeader: 14 | 'Now you will be redirected to your bank 3DSecure. If you are not redirected please refer', 15 | modalLinkLabel: 'link', 16 | }, 17 | }, 18 | init(params) { 19 | this.initParams(params) 20 | }, 21 | url(type, url) { 22 | return [ 23 | this.params.origin, 24 | this.params.endpoint[type] || '/', 25 | url || '', 26 | ].join('') 27 | }, 28 | extendParams(params) { 29 | this.utils.extend(this.params, params) 30 | return this 31 | }, 32 | initParams(params) { 33 | this.params = this.utils.extend({}, this.defaults) 34 | this.extendParams(params) 35 | this.setOrigin(this.params.origin) 36 | this.loaded = false 37 | this.created = false 38 | }, 39 | setOrigin(origin) { 40 | if (this.utils.isString(origin)) { 41 | this.params.origin = origin 42 | } 43 | return this 44 | }, 45 | scope(callback) { 46 | callback = this.proxy(callback) 47 | if (this._createFrame().loaded === true) { 48 | callback() 49 | } else { 50 | this.on('checkout.api', callback) 51 | } 52 | }, 53 | request(model, method, params) { 54 | const defer = Deferred() 55 | const data = { 56 | uid: this.connector.getUID(), 57 | action: model, 58 | method: method, 59 | params: params || {}, 60 | } 61 | this.connector.send('request', data) 62 | this.connector.on( 63 | data.uid, 64 | this.proxy(function (ev, response, model, action) { 65 | const responseModel = new Response(response) 66 | responseModel.setUID(data.uid) 67 | responseModel.setConnector(this.connector) 68 | action = 'resolveWith' 69 | if (responseModel.attr('submit3ds')) { 70 | action = 'notifyWith' 71 | } 72 | if (responseModel.attr('error')) { 73 | action = 'rejectWith' 74 | } 75 | defer[action](this, [responseModel]) 76 | }) 77 | ) 78 | return defer 79 | }, 80 | _loadFrame(url) { 81 | this.iframe = this.utils.createElement('iframe') 82 | this.addAttr(this.iframe, { 83 | allowtransparency: true, 84 | frameborder: 0, 85 | scrolling: 'no', 86 | }) 87 | this.addAttr(this.iframe, { src: url }) 88 | this.addCss(this.iframe, ApiFrameCss) 89 | this.body = this.utils.querySelector('body') 90 | if (this.body.firstChild) { 91 | this.body.insertBefore(this.iframe, this.body.firstChild) 92 | } else { 93 | this.body.appendChild(this.iframe) 94 | } 95 | return this.iframe 96 | }, 97 | _createFrame() { 98 | if (this.created === false) { 99 | this.created = true 100 | this.iframe = this._loadFrame(this.url('gateway')) 101 | this.connector = new Connector({ 102 | target: this.iframe.contentWindow, 103 | origin: this.params.origin, 104 | }) 105 | this.connector.on('load', this.proxy('_onLoadConnector')) 106 | this.connector.on('modal', this.proxy('_onOpenModal')) 107 | } 108 | return this 109 | }, 110 | _onOpenModal(xhr, model) { 111 | this.modal = new Modal({ 112 | checkout: this, 113 | model: model, 114 | }) 115 | this.modal.on('close', this.proxy('_onCloseModal')) 116 | }, 117 | _onCloseModal(modal, data) { 118 | this.trigger('modal.close', modal, data) 119 | }, 120 | _onLoadConnector() { 121 | this.loaded = true 122 | this.connector.off('load') 123 | this.trigger('checkout.api') 124 | this.off('checkout.api') 125 | }, 126 | }) 127 | -------------------------------------------------------------------------------- /src/core/class.js: -------------------------------------------------------------------------------- 1 | let init = false 2 | const fnTest = /xyz/.test( 3 | function () { 4 | return 'xyz' 5 | }.toString() 6 | ) 7 | ? /\b_super\b/ 8 | : /.*/ 9 | 10 | function ClassObject() {} 11 | 12 | ClassObject.prototype._super = function () {} 13 | 14 | ClassObject.prototype.instance = function (params) { 15 | return new this.constructor(params) 16 | } 17 | 18 | ClassObject.prototype.proxy = function (fn) { 19 | fn = typeof fn == 'string' ? this[fn] : fn 20 | return (function (cx, cb) { 21 | return function () { 22 | return cb.apply( 23 | cx, 24 | [this].concat(Array.prototype.slice.call(arguments)) 25 | ) 26 | } 27 | })(this, fn) 28 | } 29 | 30 | function superMethod(parent, name, method) { 31 | return function () { 32 | let temp = this._super, 33 | result 34 | this._super = parent[name] 35 | result = method.apply(this, arguments) 36 | this._super = temp 37 | return result 38 | } 39 | } 40 | 41 | function assign(target, instance) { 42 | let prop, 43 | proto, 44 | parent = target.prototype 45 | init = true 46 | proto = new target() 47 | init = false 48 | for (prop in instance) { 49 | if (instance.hasOwnProperty(prop)) { 50 | if ( 51 | typeof parent[prop] == 'function' && 52 | typeof instance[prop] == 'function' && 53 | fnTest.test(instance[prop]) 54 | ) { 55 | proto[prop] = superMethod(parent, prop, instance[prop]) 56 | } else { 57 | proto[prop] = instance[prop] 58 | } 59 | } 60 | } 61 | return proto 62 | } 63 | 64 | ClassObject.extend = function extend(instance) { 65 | function Class() { 66 | if (!init && this) this['init'].apply(this, arguments) 67 | } 68 | Class.prototype = assign(this, instance) 69 | Class.prototype.constructor = Class 70 | Class.extend = extend 71 | return Class 72 | } 73 | 74 | exports.ClassObject = ClassObject 75 | -------------------------------------------------------------------------------- /src/core/component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Modules registry 3 | * @type {Object} 4 | */ 5 | const modules = {} 6 | /** 7 | * @type {Object} 8 | */ 9 | const instance = {} 10 | /** 11 | * @param {string} name 12 | * @param {Object} [params] 13 | * @returns {{}} 14 | */ 15 | const newModule = function (name, params) { 16 | if (!modules[name]) { 17 | throw Error(['module is undefined', name].join(' ')) 18 | } 19 | return new modules[name](params || {}) 20 | } 21 | 22 | const addModule = function (name, module) { 23 | if (modules[name]) { 24 | throw Error(['module already added', name].join(' ')) 25 | } 26 | modules[name] = module 27 | } 28 | 29 | function Component(name, params) { 30 | if (instance[name]) return instance[name] 31 | return (instance[name] = newModule(name, params)) 32 | } 33 | 34 | Component.get = function (name, params) { 35 | return newModule(name, params) 36 | } 37 | 38 | Component.add = function (name, module) { 39 | addModule(name, module) 40 | return this 41 | } 42 | 43 | exports.Component = Component 44 | -------------------------------------------------------------------------------- /src/core/config.js: -------------------------------------------------------------------------------- 1 | exports.ApiOrigin = 'https://api.fondy.eu' 2 | 3 | exports.ApiEndpoint = { 4 | gateway: '/checkout/v2/index.html', 5 | element: '/checkout/v2/button/element.html', 6 | } 7 | 8 | exports.GooglePayApi = 'https://pay.google.com/gp/p/js/pay.js' 9 | 10 | exports.GoogleBaseRequest = { 11 | apiVersion: 2, 12 | apiVersionMinor: 0, 13 | allowedPaymentMethods: [ 14 | { 15 | type: 'CARD', 16 | parameters: { 17 | allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'], 18 | allowedCardNetworks: [ 19 | 'AMEX', 20 | 'DISCOVER', 21 | 'INTERAC', 22 | 'JCB', 23 | 'MASTERCARD', 24 | 'VISA', 25 | ], 26 | }, 27 | }, 28 | ], 29 | } 30 | 31 | exports.GooglePayLanguages = [ 32 | 'ar', 33 | 'bg', 34 | 'ca', 35 | 'zh', 36 | 'hr', 37 | 'cs', 38 | 'da', 39 | 'nl', 40 | 'en', 41 | 'et', 42 | 'fi', 43 | 'fr', 44 | 'de', 45 | 'el', 46 | 'id', 47 | 'it', 48 | 'ja', 49 | 'ko', 50 | 'ms', 51 | 'no', 52 | 'pl', 53 | 'pt', 54 | 'ru', 55 | 'sr', 56 | 'sk', 57 | 'sl', 58 | 'es', 59 | 'sv', 60 | 'th', 61 | 'tr', 62 | 'uk', 63 | ] 64 | 65 | exports.PaymentRequestOptions = {} 66 | 67 | exports.PaymentRequestDetails = { 68 | total: { 69 | label: 'Total', 70 | amount: { 71 | currency: 'USD', 72 | value: '0.00', 73 | }, 74 | }, 75 | } 76 | 77 | exports.ApiFrameCss = { 78 | width: '1px !important', 79 | height: '1px !important', 80 | left: '1px !important', 81 | bottom: '1px !important', 82 | position: 'fixed !important', 83 | border: '0px !important', 84 | } 85 | 86 | exports.ButtonFrameCss = { 87 | border: 'none !important', 88 | margin: '0 !important', 89 | padding: '0 !important', 90 | display: 'block !important', 91 | width: '1px !important', 92 | 'min-width': '100% !important', 93 | background: 'transparent !important', 94 | position: 'relative !important', 95 | opacity: '0 !important', 96 | overflow: 'hidden !important', 97 | height: '100% !important', 98 | outline: 'none !important', 99 | 'z-index': '1 !important', 100 | } 101 | 102 | exports.ButtonFrameAttrs = { 103 | tabindex: '-1', 104 | scrolling: 'no', 105 | frameborder: 0, 106 | allowtransparency: true, 107 | allowpaymentrequest: true, 108 | } 109 | 110 | exports.ButtonCoverCss = { 111 | 'z-index': '2 !important', 112 | position: 'absolute !important', 113 | border: 'none !important', 114 | background: 'transparent !important', 115 | left: '0 !important', 116 | top: '0 !important', 117 | cursor: 'pointer !important', 118 | outline: 'none !important', 119 | width: '100% !important', 120 | height: '100% !important', 121 | } 122 | 123 | exports.ButtonCoverAttrs = { 124 | role: 'button', 125 | 'aria-pressed': 'false', 126 | } 127 | 128 | exports.ButtonContainerCss = { 129 | border: '0 !important', 130 | margin: '0 !important', 131 | padding: '0 !important', 132 | display: 'block !important', 133 | background: 'transparent !important', 134 | 'user-select': 'none !important', 135 | overflow: 'hidden !important', 136 | position: 'relative !important', 137 | opacity: '1 !important', 138 | height: '0 !important', 139 | width: '100% !important', 140 | outline: 'none !important', 141 | } 142 | 143 | exports.ButtonDefaultColor = 'dark' 144 | 145 | exports.ButtonColorMap = { 146 | dark: 'dark', 147 | light: 'light', 148 | black: 'dark', 149 | white: 'light', 150 | } 151 | 152 | exports.ButtonLabelMap = { 153 | ar: '', 154 | bg: '', 155 | ca: '', 156 | zh: '', 157 | hr: '', 158 | cs: '', 159 | da: '', 160 | nl: '', 161 | en: 'Pay with', 162 | et: '', 163 | es: 'Comprar con', 164 | el: '', 165 | fi: '', 166 | fr: 'Acheter avec', 167 | de: 'Zahlen über', 168 | id: '', 169 | it: 'Acquista con', 170 | ja: '', 171 | ko: '', 172 | ms: '', 173 | no: '', 174 | pl: 'Zapłać przez', 175 | pt: '', 176 | ru: 'Оплатить через', 177 | sr: '', 178 | sk: 'Zaplatiť cez', 179 | sl: '', 180 | sv: '', 181 | th: '', 182 | tr: '', 183 | uk: 'Оплатити через', 184 | } 185 | -------------------------------------------------------------------------------- /src/core/connector.js: -------------------------------------------------------------------------------- 1 | const { Module } = require('./module') 2 | 3 | exports.Connector = Module.extend({ 4 | ns: 'crossDomain', 5 | origin: '*', 6 | uniqueId: 1, 7 | signature: null, 8 | init(params) { 9 | this.setTarget(params.target) 10 | this.setOrigin(params.origin) 11 | this.create() 12 | }, 13 | create() { 14 | this.addEvent(window, 'message', 'router') 15 | }, 16 | setOrigin(origin) { 17 | this.origin = origin || '*' 18 | }, 19 | setTarget(target) { 20 | this.target = target 21 | return this 22 | }, 23 | getUID() { 24 | return ++this.uniqueId 25 | }, 26 | destroy() { 27 | this.removeEvent(window, 'message', 'router') 28 | this._super() 29 | }, 30 | router(window, ev, response) { 31 | if (this.target !== ev.source) return false 32 | try { 33 | response = JSON.parse(ev.data) 34 | } catch (e) {} 35 | if (response && response.action && response.data) { 36 | this.trigger(response.action, response.data) 37 | } 38 | }, 39 | send(action, data, request, options) { 40 | if (!this.target) { 41 | return 42 | } 43 | request = JSON.stringify({ 44 | action: action, 45 | data: data, 46 | }) 47 | options = { 48 | targetOrigin: this.origin, 49 | delegate: 'payment', 50 | } 51 | try { 52 | this.target.postMessage(request, options) 53 | } catch (e) { 54 | this.target.postMessage(request, this.origin, []) 55 | } 56 | }, 57 | }) 58 | -------------------------------------------------------------------------------- /src/core/deferred.js: -------------------------------------------------------------------------------- 1 | const { isArray, isFunction, hasProp, forEach } = require('./utils') 2 | const PENDING = 0 3 | const RESOLVED = 1 4 | const REJECTED = 2 5 | /** 6 | * @name Deferred 7 | * @param [fn] 8 | * @return {Deferred} 9 | */ 10 | exports.Deferred = function Deferred(fn) { 11 | const doneFuncs = [] 12 | const failFuncs = [] 13 | const progressFuncs = [] 14 | let status = PENDING 15 | let resultArgs = null 16 | /** 17 | * @lends Deferred.prototype 18 | */ 19 | const promise = { 20 | done(fn) { 21 | if (status === RESOLVED) { 22 | fn.apply(this, resultArgs) 23 | } 24 | doneFuncs.push(fn) 25 | return this 26 | }, 27 | fail(fn) { 28 | if (status === RESOLVED) { 29 | fn.apply(this, resultArgs) 30 | } 31 | failFuncs.push(fn) 32 | return this 33 | }, 34 | always(fn) { 35 | return this.done(fn).fail(fn) 36 | }, 37 | progress(fn) { 38 | if (status === PENDING) { 39 | progressFuncs.push(fn) 40 | } 41 | return this 42 | }, 43 | then(done, fail, progress) { 44 | if (done) { 45 | this.done(done) 46 | } 47 | if (fail) { 48 | this.fail(fail) 49 | } 50 | if (progress) { 51 | this.progress(progress) 52 | } 53 | return this 54 | }, 55 | promise(object) { 56 | if (object === null) { 57 | return promise 58 | } 59 | forEach(promise, function (value, name) { 60 | object[name] = value 61 | }) 62 | return object 63 | }, 64 | state() { 65 | return status 66 | }, 67 | isPending() { 68 | return status === PENDING 69 | }, 70 | isRejected() { 71 | return status === REJECTED 72 | }, 73 | isResolved() { 74 | return status === RESOLVED 75 | }, 76 | } 77 | /** 78 | * @lends Deferred.prototype 79 | */ 80 | const deferred = { 81 | resolveWith(context, params) { 82 | if (status === PENDING) { 83 | status = RESOLVED 84 | let args = (resultArgs = params || []) 85 | for (let i = 0; i < doneFuncs.length; i++) { 86 | doneFuncs[i].apply(context, args) 87 | } 88 | } 89 | return this 90 | }, 91 | rejectWith(context, params) { 92 | if (status === PENDING) { 93 | status = REJECTED 94 | let args = (resultArgs = params || []) 95 | for (let i = 0; i < failFuncs.length; i++) { 96 | failFuncs[i].apply(context, args) 97 | } 98 | } 99 | return this 100 | }, 101 | notifyWith(context, params) { 102 | if (status === PENDING) { 103 | let args = (resultArgs = params || []) 104 | for (let i = 0; i < progressFuncs.length; i++) { 105 | progressFuncs[i].apply(context, args) 106 | } 107 | } 108 | return this 109 | }, 110 | resetState() { 111 | status = PENDING 112 | return this 113 | }, 114 | resolve() { 115 | return this.resolveWith(this, arguments) 116 | }, 117 | reject() { 118 | return this.rejectWith(this, arguments) 119 | }, 120 | notify() { 121 | return this.notifyWith(this, arguments) 122 | }, 123 | } 124 | const obj = promise.promise(deferred) 125 | if (isFunction(fn)) { 126 | fn.call(obj, obj) 127 | } 128 | return obj 129 | } 130 | -------------------------------------------------------------------------------- /src/core/event.js: -------------------------------------------------------------------------------- 1 | const { ClassObject } = require('./class') 2 | 3 | exports.Event = ClassObject.extend({ 4 | init() { 5 | this.events = {} 6 | this.empty = [] 7 | }, 8 | on(type, callback) { 9 | ;(this.events[type] = this.events[type] || []).push(callback) 10 | return this 11 | }, 12 | off(type, callback) { 13 | type || (this.events = {}) 14 | let list = this.events[type] || this.empty, 15 | i = (list.length = callback ? list.length : 0) 16 | while (i--) callback === list[i] && list.splice(i, 1) 17 | return this 18 | }, 19 | trigger(type) { 20 | let e = this.events[type] || this.empty, 21 | list = e.length > 0 ? e.slice(0, e.length) : e, 22 | i = 0, 23 | j 24 | while ((j = list[i++])) j.apply(j, this.empty.slice.call(arguments, 1)) 25 | return this 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /src/core/google/pay.js: -------------------------------------------------------------------------------- 1 | const { Deferred } = require('../deferred') 2 | const { Module } = require('../module') 3 | const { GooglePayApi, GoogleBaseRequest } = require('../config') 4 | 5 | const GooglePay = Module.extend({ 6 | id: 'google-payments-api', 7 | init() { 8 | this.client = null 9 | this.wrapper = this.utils.querySelector('head') 10 | this.defer = Deferred() 11 | }, 12 | load() { 13 | if (this.utils.getPath('google.payments.api.PaymentsClient')) { 14 | return this.defer.resolveWith(this) 15 | } 16 | if (this.utils.querySelector('#'.concat(this.id))) { 17 | return this.defer 18 | } 19 | this.script = this.utils.createElement('script') 20 | this.addAttr(this.script, { 21 | id: this.id, 22 | async: true, 23 | src: GooglePayApi, 24 | }) 25 | this.utils.isElement(this.wrapper) && 26 | this.wrapper.appendChild(this.script) 27 | this.addEvent(this.script, 'load', 'onLoadSuccess') 28 | this.addEvent(this.script, 'error', 'onLoadError') 29 | return this.defer 30 | }, 31 | show(methods) { 32 | const method = methods.find(function (item) { 33 | return item.supportedMethods === 'https://google.com/pay' 34 | }) 35 | const client = this.getClient({ environment: method.data.environment }) 36 | return client.loadPaymentData(method.data) 37 | }, 38 | readyToPay(cx, response) { 39 | if (response.result) { 40 | this.defer.resolveWith(this) 41 | } 42 | }, 43 | onError(cx, error) { 44 | this.defer.rejectWith(this, error) 45 | }, 46 | onLoadSuccess() { 47 | const client = this.getClient() 48 | if (client) 49 | client 50 | .isReadyToPay(GoogleBaseRequest) 51 | .then(this.proxy('readyToPay')) 52 | .catch(this.proxy('onError')) 53 | }, 54 | onLoadError() { 55 | this.defer.rejectWith(this) 56 | }, 57 | getClient(options) { 58 | if (options || this.client === null) { 59 | const PaymentClient = this.utils.getPath( 60 | 'google.payments.api.PaymentsClient' 61 | ) 62 | if (PaymentClient) { 63 | this.client = new PaymentClient(options) 64 | } else { 65 | this.onError(null, new Error('Google Client Error')) 66 | } 67 | } 68 | return this.client 69 | }, 70 | }) 71 | 72 | exports.GooglePay = new GooglePay() 73 | -------------------------------------------------------------------------------- /src/core/modal.js: -------------------------------------------------------------------------------- 1 | const { Module } = require('./module') 2 | const { Connector } = require('./connector') 3 | const { Template } = require('./template') 4 | 5 | exports.Modal = Module.extend({ 6 | init(data) { 7 | this.checkout = data.checkout 8 | this.model = data.model || {} 9 | this.messages = data.checkout.params.messages || {} 10 | this.template = new Template('acs') 11 | this.body = this.utils.querySelector('body') 12 | this.initModal() 13 | this.initConnector() 14 | }, 15 | initModal() { 16 | this.name = ['modal-iframe', this.getRandomNumber()].join('-') 17 | this.modal = this.utils.createElement('div') 18 | this.modal.innerHTML = this.template.render({ 19 | model: this.model, 20 | messages: this.messages, 21 | }) 22 | this.iframe = this.find('.ipsp-modal-iframe') 23 | this.addAttr(this.iframe, { name: this.name, id: this.name }) 24 | if (this.model['send_data']) { 25 | this.form = this.prepareForm( 26 | this.model['url'], 27 | this.model['send_data'], 28 | this.name 29 | ) 30 | this.modal.appendChild(this.form) 31 | } else { 32 | this.iframe.src = this.model['url'] 33 | } 34 | this.addEvent(this.find('.ipsp-modal-close'), 'click', 'closeModal') 35 | this.addEvent(this.find('.ipsp-modal-title a'), 'click', 'submitForm') 36 | this.initScrollbar() 37 | this.body.appendChild(this.modal) 38 | if (this.form) { 39 | this.form.submit() 40 | } 41 | }, 42 | measureScrollbar() { 43 | const scrollDiv = document.createElement('div') 44 | scrollDiv.className = 'modal-scrollbar-measure' 45 | this.body.appendChild(scrollDiv) 46 | this.utils.removeElement(scrollDiv) 47 | return scrollDiv.offsetWidth - scrollDiv.clientWidth 48 | }, 49 | checkScrollbar() { 50 | let documentElementRect 51 | let fullWindowWidth = window.innerWidth 52 | if (!fullWindowWidth) { 53 | documentElementRect = 54 | document.documentElement.getBoundingClientRect() 55 | fullWindowWidth = 56 | documentElementRect.right - Math.abs(documentElementRect.left) 57 | } 58 | this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth 59 | this.scrollbarWidth = this.measureScrollbar() 60 | }, 61 | initScrollbar() { 62 | this.checkScrollbar() 63 | this.bodyPad = parseInt( 64 | this.utils.getStyle(this.body, 'padding-right') || 0, 65 | 10 66 | ) 67 | this.originalBodyPad = document.body.style.paddingRight || '' 68 | this.originalOverflow = document.body.style.overflow || '' 69 | if (this.bodyIsOverflowing) { 70 | this.addCss(this.body, { 71 | paddingRight: [this.bodyPad + this.scrollbarWidth, 'px'].join( 72 | '' 73 | ), 74 | overflow: 'hidden', 75 | }) 76 | } 77 | }, 78 | resetScrollbar() { 79 | this.addCss(this.body, { 80 | paddingRight: this.originalBodyPad 81 | ? [this.originalBodyPad, 'px'].join('') 82 | : '', 83 | overflow: this.originalOverflow, 84 | }) 85 | }, 86 | getRandomNumber() { 87 | return Math.round(Math.random() * 1000000000) 88 | }, 89 | find(selector) { 90 | return this.utils.querySelector(selector, this.modal) 91 | }, 92 | closeModal(el, ev) { 93 | ev.preventDefault() 94 | this.trigger('close', this.data) 95 | this.removeModal() 96 | }, 97 | submitForm(el, ev) { 98 | ev.preventDefault() 99 | this.trigger('submit', this.data) 100 | this.addAttr(this.form, { 101 | target: '_blank', 102 | }) 103 | this.form.submit() 104 | }, 105 | removeModal() { 106 | this.destroy() 107 | }, 108 | destroy() { 109 | this.utils.removeElement(this.modal) 110 | this.resetScrollbar() 111 | this.connector.destroy() 112 | this._super() 113 | }, 114 | initConnector() { 115 | this.connector = new Connector({ target: this.iframe.contentWindow }) 116 | this.connector.on('response', this.proxy('onResponse')) 117 | }, 118 | onResponse(ev, data) { 119 | this.sendResponse(data) 120 | this.removeModal() 121 | }, 122 | sendResponse(data) { 123 | this.checkout.connector.send('request', { 124 | uid: data.uid, 125 | action: 'api.checkout.proxy', 126 | method: 'send', 127 | params: data, 128 | }) 129 | }, 130 | }) 131 | -------------------------------------------------------------------------------- /src/core/model.js: -------------------------------------------------------------------------------- 1 | const { Module } = require('./module') 2 | 3 | exports.Model = Module.extend({ 4 | init(data) { 5 | this.data = data || {} 6 | this.create() 7 | }, 8 | create() {}, 9 | eachProps(args) { 10 | const name = args[1] ? args[0] : null 11 | const callback = args[1] ? args[1] : args[0] 12 | const list = name ? this.alt(name, []) : this.data 13 | return { 14 | list: list, 15 | callback: callback, 16 | } 17 | }, 18 | each() { 19 | const props = this.eachProps(arguments) 20 | for (let prop in props.list) { 21 | if (props.list.hasOwnProperty(prop)) { 22 | props.callback( 23 | this.instance(props.list[prop]), 24 | props.list[prop], 25 | prop 26 | ) 27 | } 28 | } 29 | }, 30 | filter() { 31 | const props = this.eachProps(arguments) 32 | for (let prop in props.list) { 33 | if (props.list.hasOwnProperty(prop)) { 34 | const item = this.instance(props.list[prop]) 35 | if (props.callback(item, props.list[prop], prop)) { 36 | return props.list[prop] 37 | } 38 | } 39 | } 40 | }, 41 | find() { 42 | const props = this.eachProps(arguments) 43 | for (let prop in props.list) { 44 | if (props.list.hasOwnProperty(prop)) { 45 | const item = this.instance(props.list[prop]) 46 | if (props.callback(item, props.list[prop], prop)) { 47 | return item 48 | } 49 | } 50 | } 51 | return false 52 | }, 53 | alt(prop, defaults) { 54 | prop = this.attr(prop) 55 | return typeof prop === 'undefined' ? defaults : prop 56 | }, 57 | attr(key, value) { 58 | let i = 0, 59 | data = this.data, 60 | name = (key || '').split('.'), 61 | prop = name.pop(), 62 | len = arguments.length 63 | for (; i < name.length; i++) { 64 | if (data && data.hasOwnProperty(name[i])) { 65 | data = data[name[i]] 66 | } else { 67 | if (len === 2) { 68 | data = data[name[i]] = {} 69 | } else { 70 | break 71 | } 72 | } 73 | } 74 | if (len === 1) { 75 | return data ? data[prop] : undefined 76 | } 77 | if (len === 2) { 78 | data[prop] = value 79 | } 80 | return this 81 | }, 82 | stringify() { 83 | return JSON.stringify(this.serialize()) 84 | }, 85 | serialize() { 86 | return this.data 87 | }, 88 | }) 89 | -------------------------------------------------------------------------------- /src/core/module.js: -------------------------------------------------------------------------------- 1 | const { ClassObject } = require('./class') 2 | const { Event } = require('./event') 3 | const Utils = require('./utils') 4 | 5 | exports.Module = ClassObject.extend({ 6 | utils: Utils, 7 | getListener() { 8 | if (!this._listener_) this._listener_ = new Event() 9 | return this._listener_ 10 | }, 11 | destroy() { 12 | this.off() 13 | }, 14 | on(type, callback) { 15 | this.getListener().on(type, callback) 16 | return this 17 | }, 18 | off(type, callback) { 19 | this.getListener().off(type, callback) 20 | return this 21 | }, 22 | proxy(fn) { 23 | if (!this._proxy_cache_) this._proxy_cache_ = {} 24 | if (this.utils.isString(fn)) { 25 | if (!this._proxy_cache_[fn]) { 26 | this._proxy_cache_[fn] = this._super(fn) 27 | } 28 | return this._proxy_cache_[fn] 29 | } 30 | return this._super(fn) 31 | }, 32 | trigger() { 33 | this.getListener().trigger.apply(this.getListener(), arguments) 34 | return this 35 | }, 36 | each(ob, cb) { 37 | this.utils.forEach(ob, this.proxy(cb)) 38 | }, 39 | addAttr(el, ob) { 40 | if (!this.utils.isElement(el)) return false 41 | this.utils.forEach(ob, function (v, k) { 42 | el.setAttribute(k, v) 43 | }) 44 | }, 45 | addCss(el, ob) { 46 | if (!this.utils.isElement(el)) return false 47 | this.utils.forEach( 48 | ob, 49 | function (v, k) { 50 | this.addCssProperty(el, k, v) 51 | }, 52 | this 53 | ) 54 | }, 55 | addCssProperty(el, style, value) { 56 | let result = el.style.cssText.match( 57 | new RegExp( 58 | '(?:[;\\s]|^)(' + 59 | style.replace('-', '\\-') + 60 | '\\s*:(.*?)(;|$))' 61 | ) 62 | ), 63 | idx 64 | if (result) { 65 | idx = result.index + result[0].indexOf(result[1]) 66 | el.style.cssText = 67 | el.style.cssText.substring(0, idx) + 68 | style + 69 | ': ' + 70 | value + 71 | ';' + 72 | el.style.cssText.substring(idx + result[1].length) 73 | } else { 74 | el.style.cssText += ' ' + style + ': ' + value + ';' 75 | } 76 | }, 77 | addEvent(el, ev, cb) { 78 | if (!el || !ev || !cb) return false 79 | cb = this.proxy(cb) 80 | if (el.addEventListener) el.addEventListener(ev, cb) 81 | else if (el['attachEvent']) el['attachEvent']('on' + ev, cb) 82 | }, 83 | removeEvent(el, ev, cb) { 84 | if (!el || !ev || !cb) return false 85 | cb = this.proxy(cb) 86 | if (el.removeEventListener) el.removeEventListener(ev, cb, false) 87 | else if (el['detachEvent']) el['detachEvent']('on' + ev, cb) 88 | }, 89 | prepareForm(url, data, target, method) { 90 | const form = this.utils.createElement('form') 91 | this.addAttr(form, { 92 | action: url, 93 | target: target || '_self', 94 | method: method || 'POST', 95 | }) 96 | this.addCss(form, { 97 | display: 'none', 98 | }) 99 | this.utils.forEach( 100 | data, 101 | function (v, k, el) { 102 | el = this.utils.createElement('input') 103 | el.type = 'hidden' 104 | el.name = k 105 | el.value = v 106 | form.appendChild(el) 107 | }, 108 | this 109 | ) 110 | return form 111 | }, 112 | }) 113 | -------------------------------------------------------------------------------- /src/core/payment/button.js: -------------------------------------------------------------------------------- 1 | const { Module } = require('../module') 2 | const { Api } = require('../api') 3 | const { PaymentRequestApi } = require('./request') 4 | const { PaymentElement } = require('./element') 5 | const { forEach } = require('../utils') 6 | const { ApiOrigin, ApiEndpoint } = require('../config') 7 | 8 | exports.PaymentButton = Module.extend({ 9 | defaults: { 10 | origin: ApiOrigin, 11 | endpoint: ApiEndpoint, 12 | methods: ['apple', 'google'], 13 | element: null, 14 | style: { 15 | height: 38, 16 | mode: 'default', 17 | type: 'long', 18 | color: 'black', 19 | }, 20 | data: { 21 | lang: 'en', 22 | }, 23 | before(defer) { 24 | defer.resolve() 25 | }, 26 | }, 27 | init(params) { 28 | this.elements = {} 29 | this.supported = false 30 | this.params = { 31 | element: params.element, 32 | methods: params.methods || this.defaults.methods, 33 | origin: params.origin || this.defaults.origin, 34 | endpoint: this.utils.extend( 35 | {}, 36 | this.defaults.endpoint, 37 | params.endpoint 38 | ), 39 | style: this.utils.extend({}, this.defaults.style, params.style), 40 | before: params.before || this.defaults.before, 41 | data: this.utils.extend({}, this.defaults.data, params.data), 42 | } 43 | this.initApi(params.api) 44 | this.initPaymentRequestApi() 45 | this.initElements() 46 | this.update() 47 | }, 48 | initApi(api) { 49 | if (api instanceof Api) { 50 | this.api = api 51 | } else { 52 | this.api = new Api({ 53 | origin: this.params.origin, 54 | endpoint: this.params.endpoint, 55 | }) 56 | } 57 | }, 58 | initPaymentRequestApi() { 59 | this.request = new PaymentRequestApi() 60 | this.request.setApi(this.api) 61 | this.request.setMerchant(this.params.data.merchant_id) 62 | this.request.setBeforeCallback(this.params.before) 63 | this.request.on('details', this.proxy('onDetails')) 64 | this.request.on('error', this.proxy('onError')) 65 | }, 66 | initElements() { 67 | const style = this.params.style 68 | const data = this.params.data 69 | const origin = this.params.origin 70 | const appendTo = this.params.element 71 | const endpoint = this.params.endpoint.element 72 | const request = this.request 73 | this.container = this.utils.querySelector(this.params.element) 74 | this.addCss(this.container, { 75 | display: 'flex', 76 | gap: '1rem', 77 | 'flex-direction': 'column', 78 | }) 79 | forEach(this.params.methods, function (method) { 80 | const element = new PaymentElement({ 81 | origin: origin, 82 | endpoint: endpoint, 83 | method: method, 84 | appendTo: appendTo, 85 | color: style.color, 86 | mode: style.mode, 87 | lang: data.lang, 88 | height: style.height, 89 | }) 90 | element.setPaymentRequest(request) 91 | }) 92 | request.getSupportedMethods() 93 | }, 94 | update(data) { 95 | return this.request.update( 96 | this.utils.extend(this.params.data, data || {}) 97 | ) 98 | }, 99 | onDetails(cx, data) { 100 | this.api.scope(() => { 101 | this.api 102 | .request( 103 | 'api.checkout.form', 104 | 'request', 105 | this.utils.extend({}, this.params.data, data) 106 | ) 107 | .done(this.proxy('onSuccess')) 108 | .fail(this.proxy('onError')) 109 | }) 110 | }, 111 | onSuccess(cx, data) { 112 | this.trigger('success', data) 113 | }, 114 | onError(cx, data) { 115 | this.trigger('error', data) 116 | }, 117 | }) 118 | -------------------------------------------------------------------------------- /src/core/payment/container.js: -------------------------------------------------------------------------------- 1 | const { Module } = require('../module') 2 | const { Connector } = require('../connector') 3 | const { PaymentRequestApi } = require('./request') 4 | const { 5 | GooglePayLanguages, 6 | ButtonColorMap, 7 | ButtonDefaultColor, 8 | ButtonLabelMap, 9 | } = require('../config') 10 | 11 | const parseConfig = (config) => { 12 | const data = { 13 | payment_system: config.payment_system, 14 | provider: {}, 15 | } 16 | const methods = config.methods || [] 17 | const details = config.details || {} 18 | const options = config.options || {} 19 | const regex = new RegExp('//([a-z]+)\\.com/') 20 | methods.forEach(function (item) { 21 | const match = regex.exec(item.supportedMethods) 22 | if (match === null) return 23 | data.provider[match[1]] = { 24 | methods: [item], 25 | details: details, 26 | options: options, 27 | } 28 | }) 29 | return data 30 | } 31 | 32 | exports.PaymentContainer = Module.extend({ 33 | defaults: { 34 | element: null, 35 | method: 'card', 36 | data: { 37 | lang: 'en', 38 | }, 39 | style: {}, 40 | }, 41 | init(params) { 42 | this.initParams(params) 43 | this.initEvents() 44 | }, 45 | initParams(params) { 46 | this.params = this.utils.extend({}, this.defaults, params) 47 | this.element = this.utils.querySelector(this.params.element) 48 | this.connector = new Connector({ 49 | target: window.parent, 50 | }) 51 | this.payment = new PaymentRequestApi({ 52 | embedded: true, 53 | }) 54 | }, 55 | extendParams(params) { 56 | this.utils.extend(this.params, { 57 | method: params.method, 58 | style: params.style, 59 | data: params.data, 60 | css: params.css, 61 | }) 62 | }, 63 | getGoogleLangSupport(lang, defaults) { 64 | return GooglePayLanguages.indexOf(lang) !== -1 ? lang : defaults 65 | }, 66 | getButtonColor(color) { 67 | return ButtonColorMap[color] || ButtonDefaultColor 68 | }, 69 | getGoogleSvg(color, lang, mode) { 70 | const params = { 71 | endpoint: 'https://www.gstatic.com/instantbuy/svg', 72 | color: this.getButtonColor(color), 73 | mode: mode || 'plain', 74 | lang: lang || 'en', 75 | } 76 | let format = 'url("{endpoint}/{color}/{mode}/{lang}.svg")' 77 | if (mode === 'plain') { 78 | format = 'url("{endpoint}/{color}_gpay.svg")' 79 | } 80 | if (mode === 'buy') { 81 | format = 'url("{endpoint}/{color}/{lang}.svg")' 82 | } 83 | return this.utils.stringFormat(format, params) 84 | }, 85 | getAppleSvg(color) { 86 | const format = 'url("svg/apple-pay-{color}.svg")' 87 | const params = { 88 | color: this.getButtonColor(color), 89 | } 90 | return this.utils.stringFormat(format, params) 91 | }, 92 | getAppleLabel(lang) { 93 | return ButtonLabelMap[lang || 'en'] 94 | }, 95 | addFrameImage() { 96 | const frame = 97 | this.utils.querySelector('iframe', this.element) || 98 | this.utils.createElement('iframe') 99 | const url = 'https://pay.google.com/gp/p/generate_gpay_btn_img' 100 | const style = this.params.style || {} 101 | const lang = this.getGoogleLangSupport(this.params.data.lang, 'en') 102 | const query = { 103 | buttonColor: style.color || 'black', 104 | browserLocale: lang, 105 | buttonSizeMode: 'fill', 106 | } 107 | const src = [url, this.utils.param(query)].join('?') 108 | this.addAttr(frame, { 109 | scrolling: 'no', 110 | frameborder: 0, 111 | src: src, 112 | }) 113 | this.element.appendChild(frame) 114 | this.element.classList.remove('short', 'long') 115 | }, 116 | styleButton() { 117 | let element = this.element 118 | let params = this.params 119 | let method = params.method 120 | let style = params.style || {} 121 | let lang = params.data.lang || 'en' 122 | let css = params.css || {} 123 | element.setAttribute('class', '') 124 | element.classList.add('button', 'pending') 125 | if (method === 'card') method = 'google' 126 | if (method) { 127 | element.classList.add(method) 128 | } 129 | if (lang) { 130 | element.classList.add(lang) 131 | } 132 | if (style.type) { 133 | element.classList.add(style.type) 134 | } 135 | if (style.mode) { 136 | element.classList.add(style.mode) 137 | } 138 | if (style.color) { 139 | element.classList.add(style.color) 140 | } 141 | if (method === 'google') { 142 | if (style.type === 'short') { 143 | style.mode = 'plain' 144 | } 145 | if (style.mode === 'default') { 146 | this.addFrameImage() 147 | } else { 148 | css.image = this.getGoogleSvg(style.color, lang, style.mode) 149 | } 150 | } 151 | if (method === 'apple') { 152 | css.image = this.getAppleSvg(style.color) 153 | css.label = this.getAppleLabel(lang, style.mode) 154 | } 155 | if (css) { 156 | this.utils.forEach(css, function (value, name) { 157 | element.style.setProperty(['--', name].join(''), value) 158 | }) 159 | } 160 | }, 161 | initEvents() { 162 | this.payment.on( 163 | 'details', 164 | this.proxy(function (cx, data) { 165 | this.connector.send('details', data) 166 | this.connector.send('complete', data) 167 | }) 168 | ) 169 | this.payment.on( 170 | 'reload', 171 | this.proxy(function (cx, data) { 172 | this.connector.send('reload', data) 173 | }) 174 | ) 175 | this.payment.on( 176 | 'error', 177 | this.proxy(function (cx, data) { 178 | this.connector.send('error', data) 179 | }) 180 | ) 181 | this.connector.on( 182 | 'options', 183 | this.proxy(function (cx, data) { 184 | this.extendParams(data) 185 | this.styleButton() 186 | }) 187 | ) 188 | this.connector.on( 189 | 'pay', 190 | this.proxy(function () { 191 | if (!this.element.classList.contains('pending')) { 192 | if (this.params.method === 'apple') { 193 | const payload = this.payment.payload || {} 194 | const apple = payload.provider['apple'] || {} 195 | this.connector.send('pay', { 196 | payment_system: payload.payment_system, 197 | methods: apple.methods, 198 | details: apple.details, 199 | options: apple.options, 200 | }) 201 | } else { 202 | this.payment 203 | .setSupported({ 204 | fallback: false, 205 | provider: ['google'], 206 | }) 207 | .pay('google') 208 | } 209 | } 210 | }) 211 | ) 212 | this.connector.on( 213 | 'config', 214 | this.proxy(function (cx, data) { 215 | this.payment.setPayload((data = parseConfig(data))) 216 | if ( 217 | data.payment_system && 218 | Object.keys(data.provider).length > 0 219 | ) { 220 | this.element.classList.remove('pending') 221 | this.element.classList.add('ready') 222 | this.connector.send('show', {}) 223 | } else { 224 | this.connector.send('hide', {}) 225 | } 226 | }) 227 | ) 228 | this.connector.on( 229 | 'event', 230 | this.proxy(function (cx, data) { 231 | if (data.type === 'mouseenter') { 232 | this.element.classList.add('hover') 233 | } 234 | if (data.type === 'mouseleave') { 235 | this.element.classList.remove('hover') 236 | } 237 | if (data.type === 'focusin') { 238 | this.element.classList.add('active') 239 | } 240 | if (data.type === 'focusout') { 241 | this.element.classList.remove('active') 242 | } 243 | }) 244 | ) 245 | }, 246 | }) 247 | -------------------------------------------------------------------------------- /src/core/payment/element.js: -------------------------------------------------------------------------------- 1 | const { Module } = require('../module') 2 | const { Connector } = require('../connector') 3 | const { 4 | ButtonContainerCss, 5 | ButtonCoverCss, 6 | ButtonCoverAttrs, 7 | ButtonFrameCss, 8 | ButtonFrameAttrs, 9 | } = require('../config') 10 | 11 | const { PaymentRequestApi } = require('./request') 12 | 13 | exports.PaymentElement = Module.extend({ 14 | defaults: { 15 | method: null, 16 | appendTo: null, 17 | className: 'payment-element', 18 | origin: 'https://pay.fondy.eu', 19 | endpoint: '/checkout/v2/button/element.html', 20 | mode: 'plain', 21 | color: 'black', 22 | lang: 'en', 23 | height: 38, 24 | }, 25 | init(params) { 26 | this.params = {} 27 | this.utils.extend(this.params, this.defaults) 28 | this.utils.extend(this.params, params) 29 | this.initElement() 30 | }, 31 | getElementUrl() { 32 | return [this.params.origin, this.params.endpoint].join('') 33 | }, 34 | getElementOptions() { 35 | return this.utils.param({ 36 | method: this.params.method, 37 | mode: this.params.mode, 38 | style: this.params.style, 39 | color: this.params.color, 40 | lang: this.params.lang, 41 | }) 42 | }, 43 | initElement() { 44 | this.element = this.utils.createElement('div') 45 | this.iframe = this.utils.createElement('iframe') 46 | this.button = this.utils.createElement('a') 47 | this.addCss(this.element, ButtonContainerCss) 48 | this.addCss(this.button, ButtonCoverCss) 49 | this.addAttr(this.button, ButtonCoverAttrs) 50 | this.addCss(this.iframe, ButtonFrameCss) 51 | this.addAttr(this.iframe, ButtonFrameAttrs) 52 | this.addAttr(this.iframe, { 53 | src: [this.getElementUrl(), this.getElementOptions()].join('?'), 54 | }) 55 | this.addEvent(this.iframe, 'load', 'onloadConnector') 56 | this.addEvent(this.iframe, 'error', 'errorConnector') 57 | this.addAttr(this.element, { 58 | class: this.params.className, 59 | }) 60 | this.element.appendChild(this.iframe) 61 | this.element.appendChild(this.button) 62 | }, 63 | errorConnector() { 64 | log('frame load error') 65 | }, 66 | onloadConnector() { 67 | this.connector = new Connector({ 68 | target: this.iframe.contentWindow, 69 | origin: this.params.origin, 70 | }) 71 | }, 72 | send(action, data) { 73 | if (this.connector) { 74 | this.connector.send(action, data) 75 | } 76 | }, 77 | onEvent(cx, ev) { 78 | ev.preventDefault() 79 | if (this.pending) return false 80 | this.send('event', { type: ev.type }) 81 | }, 82 | onClick() { 83 | if (this.pending) return false 84 | this.request 85 | .before() 86 | .done(this.proxy('onClickDone')) 87 | .fail(this.proxy('onClickFail')) 88 | }, 89 | onClickDone() { 90 | this.request.pay(this.params.method) 91 | }, 92 | onClickFail() {}, 93 | onSupported(cx, supported) { 94 | if (supported.provider.includes(this.params.method)) { 95 | this.mount() 96 | } 97 | }, 98 | onPayload(cx, payload) { 99 | if (payload.allowed.includes(this.params.method)) { 100 | this.show() 101 | } 102 | }, 103 | onPending(cx, state) { 104 | this.pending = state 105 | this.send('pending', { state: state }) 106 | }, 107 | initEvents() { 108 | this.addEvent(this.button, 'mouseenter', 'onEvent') 109 | this.addEvent(this.button, 'mouseleave', 'onEvent') 110 | this.addEvent(this.button, 'blur', 'onEvent') 111 | this.addEvent(this.button, 'focus', 'onEvent') 112 | this.addEvent(this.button, 'click', 'onClick') 113 | }, 114 | setPaymentRequest(request) { 115 | if (!(request instanceof PaymentRequestApi)) 116 | throw Error('request is not instance of PaymentRequestApi') 117 | this.request = request 118 | const onSupported = this.proxy('onSupported') 119 | const onPayload = this.proxy('onPayload') 120 | const onPending = this.proxy('onPending') 121 | this.request.off('supported', onSupported).on('supported', onSupported) 122 | this.request.off('payload', onPayload).on('payload', onPayload) 123 | this.request.off('pending', onPending).on('pending', onPending) 124 | return this 125 | }, 126 | appendTo(appendTo) { 127 | const container = this.utils.querySelector(appendTo) 128 | if (container) container.appendChild(this.element) 129 | this.initEvents() 130 | return this 131 | }, 132 | mount() { 133 | this.appendTo(this.params.appendTo) 134 | }, 135 | isMounted() { 136 | return document.body.contains(this.element) 137 | }, 138 | show() { 139 | if (this.isMounted() === false) return this 140 | this.addCss(this.iframe, { 141 | transition: 'opacity 0.6s 0.4s ease-out', 142 | opacity: this.utils.cssUnit(1), 143 | }) 144 | this.addCss(this.element, { 145 | transition: 'height 0.2s ease-out', 146 | height: this.utils.cssUnit(this.params.height, 'px'), 147 | }) 148 | this.trigger('show', {}) 149 | return this 150 | }, 151 | hide() { 152 | if (this.isMounted() === false) return this 153 | this.addCss(this.iframe, { 154 | transition: 'opacity 0.4s ease-out', 155 | opacity: this.utils.cssUnit(0), 156 | }) 157 | this.addCss(this.element, { 158 | transition: 'height 0.2s 0.4s ease-out', 159 | height: this.utils.cssUnit(0, 'px'), 160 | }) 161 | this.trigger('hide', {}) 162 | return this 163 | }, 164 | }) 165 | -------------------------------------------------------------------------------- /src/core/payment/request.js: -------------------------------------------------------------------------------- 1 | const { Module } = require('../module') 2 | const { Api } = require('../api') 3 | const { GooglePay } = require('../google/pay') 4 | const { Deferred } = require('../deferred') 5 | const { getPaymentRequest, hasProp, isFunction } = require('../utils') 6 | const { GoogleBaseRequest, PaymentRequestDetails } = require('../config') 7 | 8 | const getPaymentMethods = () => { 9 | return [ 10 | [ 11 | 'google', 12 | { 13 | supportedMethods: 'https://google.com/pay', 14 | data: GoogleBaseRequest, 15 | }, 16 | ], 17 | [ 18 | 'apple', 19 | { 20 | supportedMethods: 'https://apple.com/apple-pay', 21 | }, 22 | () => hasProp(window, 'ApplePaySession'), 23 | ], 24 | ] 25 | } 26 | 27 | let requestDeferred = null 28 | 29 | let requestSupported = { 30 | fallback: false, 31 | provider: [], 32 | } 33 | 34 | const getSupportedMethods = () => { 35 | if (requestDeferred) return requestDeferred 36 | requestDeferred = Deferred() 37 | const methods = getPaymentMethods() 38 | const details = PaymentRequestDetails 39 | ;(function check() { 40 | const item = methods.shift() 41 | if (item === undefined) { 42 | if (requestSupported.provider.indexOf('google') === -1) { 43 | requestSupported.fallback = true 44 | requestSupported.provider.push('google') 45 | } 46 | return requestDeferred.resolve(requestSupported) 47 | } 48 | const method = item.shift() 49 | const config = item.shift() 50 | const callback = item.shift() 51 | if (isFunction(callback) && callback() === false) { 52 | setTimeout(check, 25) 53 | return false 54 | } 55 | const request = getPaymentRequest([config], details, {}) 56 | if (request) { 57 | request 58 | .canMakePayment() 59 | .then(function (status) { 60 | if (status === true) requestSupported.provider.push(method) 61 | setTimeout(check, 25) 62 | }) 63 | .catch(function () { 64 | setTimeout(check, 25) 65 | }) 66 | } else { 67 | setTimeout(check, 25) 68 | } 69 | })() 70 | return requestDeferred 71 | } 72 | 73 | const PaymentRequestInterface = Module.extend({ 74 | config: { 75 | payment_system: '', 76 | fallback: false, 77 | methods: [], 78 | details: {}, 79 | options: {}, 80 | }, 81 | supported: { 82 | fallback: false, 83 | provider: [], 84 | }, 85 | payload: { 86 | payment_system: null, 87 | provider: [], 88 | }, 89 | params: {}, 90 | getSupportedMethods() { 91 | return getSupportedMethods().then((supported) => { 92 | this.setSupported(supported) 93 | return supported 94 | }) 95 | }, 96 | init(params) { 97 | this.params = params || {} 98 | }, 99 | setSupported(supported) { 100 | this.supported = supported 101 | this.trigger('supported', supported) 102 | return this 103 | }, 104 | setPayload(payload) { 105 | this.payload = payload 106 | this.trigger('payload', payload) 107 | return this 108 | }, 109 | setMerchant(merchant) { 110 | this.merchant = merchant 111 | }, 112 | setApi(api) { 113 | if (api instanceof Api) this.api = api 114 | return this 115 | }, 116 | getProviderPayload(method) { 117 | return this.payload.provider[method] || {} 118 | }, 119 | isMethodSupported(method) { 120 | return this.supported.provider.indexOf(method) !== -1 121 | }, 122 | isFallbackMethod(method) { 123 | return method === 'google' && this.supported.fallback 124 | }, 125 | }) 126 | /** 127 | * @constructor 128 | */ 129 | const PaymentRequestApi = PaymentRequestInterface.extend({ 130 | request(method, params, success, failure) { 131 | if (this.api) { 132 | this.api.scope( 133 | this.proxy(function () { 134 | this.api 135 | .request('api.checkout.pay', method, params) 136 | .done(this.proxy(success)) 137 | .fail(this.proxy(failure)) 138 | }) 139 | ) 140 | } 141 | }, 142 | update(data) { 143 | const defer = Deferred() 144 | this.request( 145 | 'methods', 146 | data, 147 | function (cx, model) { 148 | this.onUpdate(cx, model) 149 | defer.resolveWith(this, [model]) 150 | }, 151 | function (cx, model) { 152 | this.onError(cx, model) 153 | defer.rejectWith(this, [model]) 154 | } 155 | ) 156 | return defer 157 | }, 158 | onUpdate(cx, model) { 159 | this.setPayload(model.serialize()) 160 | }, 161 | onError(cx, model) { 162 | this.trigger('error', model) 163 | }, 164 | isPending() { 165 | return this.pendingState === true 166 | }, 167 | setPending(state) { 168 | this.pendingState = state 169 | setTimeout(() => { 170 | this.trigger('pending', state) 171 | }, 100) 172 | }, 173 | beforeCallback(defer) { 174 | defer.resolve() 175 | }, 176 | setBeforeCallback(callback) { 177 | if (isFunction(callback)) { 178 | this.beforeCallback = callback 179 | } 180 | return this 181 | }, 182 | before() { 183 | if (this.isPending()) return 184 | this.setPending(true) 185 | const defer = Deferred() 186 | defer.always( 187 | this.proxy(function () { 188 | this.setPending(false) 189 | }) 190 | ) 191 | this.beforeCallback(defer) 192 | return defer 193 | }, 194 | pay(method) { 195 | if (this.isPending()) return 196 | this.setPending(true) 197 | const payload = this.getProviderPayload(method) 198 | const response = Deferred() 199 | response.always(function () { 200 | this.setPending(false) 201 | }) 202 | if (this.isMethodSupported(method) === false) { 203 | return response.rejectWith(this, [{ test: true }]) 204 | } 205 | if (this.isFallbackMethod(method)) { 206 | this.makePaymentFallback(response, payload.methods) 207 | } else { 208 | this.makeNativePayment(response, payload) 209 | } 210 | return response 211 | .done(function (details) { 212 | this.trigger('details', { 213 | payment_system: this.payload.payment_system, 214 | data: details, 215 | }) 216 | }) 217 | .fail(function (error) { 218 | this.trigger('error', error) 219 | if (this.params.embedded === true) { 220 | location.reload() 221 | this.trigger('reload', this.params) 222 | } 223 | }) 224 | }, 225 | makeNativePayment(defer, payload) { 226 | const self = this 227 | const request = getPaymentRequest( 228 | payload.methods, 229 | payload.details, 230 | payload.options 231 | ) 232 | this.addEvent(request, 'merchantvalidation', 'merchantValidation') 233 | request 234 | .canMakePayment() 235 | .then(function () { 236 | request 237 | .show() 238 | .then(function (response) { 239 | response.complete('success').then(function () { 240 | defer.resolveWith(self, [response.details]) 241 | }) 242 | }) 243 | .catch(function (e) { 244 | defer.rejectWith(self, [ 245 | { code: e.code, message: e.message }, 246 | ]) 247 | }) 248 | }) 249 | .catch(function (e) { 250 | defer.rejectWith(self, [{ code: e.code, message: e.message }]) 251 | }) 252 | }, 253 | makePaymentFallback(defer, methods) { 254 | const self = this 255 | GooglePay.load().then(() => { 256 | GooglePay.show(methods) 257 | .then((details) => { 258 | defer.resolveWith(self, [details]) 259 | }) 260 | .catch((e) => { 261 | defer.rejectWith(self, [ 262 | { code: e.code, message: e.message }, 263 | ]) 264 | }) 265 | }) 266 | }, 267 | appleSession(params) { 268 | const defer = Deferred() 269 | this.request( 270 | 'session', 271 | params, 272 | function (c, model) { 273 | defer.resolveWith(this, [model.serialize()]) 274 | }, 275 | function (c, model) { 276 | defer.rejectWith(this, [model]) 277 | } 278 | ) 279 | return defer 280 | }, 281 | merchantValidation(cx, event) { 282 | const { validationURL } = event 283 | const { host } = location 284 | this.appleSession({ 285 | url: validationURL, 286 | domain: host, 287 | merchant_id: this.merchant, 288 | }) 289 | .done(function (session) { 290 | try { 291 | event.complete(session.data) 292 | } catch (error) { 293 | this.trigger('error', error) 294 | } 295 | }) 296 | .fail(function (error) { 297 | this.trigger('error', error) 298 | }) 299 | }, 300 | }) 301 | 302 | exports.PaymentRequestApi = PaymentRequestApi 303 | -------------------------------------------------------------------------------- /src/core/response.js: -------------------------------------------------------------------------------- 1 | const { Model } = require('./model') 2 | 3 | const ProxyUrl = 'http://secure-redirect.cloudipsp.com/submit/' 4 | 5 | exports.Response = Model.extend({ 6 | stringFormat(string) { 7 | const that = this 8 | return (string || '').replace(/{(.+?)}/g, function (match, prop) { 9 | return that.attr(['order.order_data', prop].join('.')) || match 10 | }) 11 | }, 12 | setConnector(connector) { 13 | this.connector = connector 14 | return this 15 | }, 16 | setUID(uid) { 17 | this.uid = uid 18 | return this 19 | }, 20 | getUID() { 21 | return this.uid 22 | }, 23 | formDataProxy(url, data, target, method) { 24 | location.assign( 25 | [ 26 | ProxyUrl, 27 | JSON.stringify({ 28 | url: url, 29 | params: data, 30 | target: target, 31 | method: method, 32 | }), 33 | ].join('#') 34 | ) 35 | }, 36 | formDataSubmit(url, data, target, method) { 37 | if (url.match(/^http:/)) { 38 | return this.formDataProxy(url, data, target, method) 39 | } 40 | const action = this.stringFormat(url) 41 | const form = this.prepareForm(action, data, target, method) 42 | const body = this.utils.querySelector('body') 43 | body.appendChild(form) 44 | form.submit() 45 | form.parentNode.removeChild(form) 46 | }, 47 | inProgress() { 48 | return this.attr('order.in_progress') 49 | }, 50 | readyToSubmit() { 51 | return this.attr('order.ready_to_submit') 52 | }, 53 | waitForResponse() { 54 | return this.attr('order.pending') 55 | }, 56 | needVerifyCode() { 57 | return this.attr('order.need_verify_code') 58 | }, 59 | redirectUrl() { 60 | if (this.attr('url')) { 61 | this.redirectToUrl(this.attr('url')) 62 | return true 63 | } 64 | return false 65 | }, 66 | redirectToUrl(url) { 67 | location.assign(url) 68 | }, 69 | submitToMerchant() { 70 | const ready = this.attr('order.ready_to_submit') 71 | const url = this.attr('model.url') || this.attr('order.response_url') 72 | const method = this.attr('order.method') 73 | const action = this.attr('order.action') 74 | const data = 75 | this.attr('model.send_data') || this.attr('order.order_data') 76 | if (ready && url && data) { 77 | if ( 78 | action === 'redirect' || 79 | data['get_without_parameters'] === true 80 | ) { 81 | this.redirectToUrl(url) 82 | } else { 83 | this.formDataSubmit(url, data, '_self', method) 84 | } 85 | return true 86 | } 87 | }, 88 | submitForm() { 89 | const method = this.attr('method') 90 | const url = this.attr('url') 91 | const data = this.attr('send_data') 92 | if (url && data) { 93 | this.formDataSubmit(url, data, '_self', method) 94 | return true 95 | } 96 | return false 97 | }, 98 | sendResponse() { 99 | const action = this.attr('action') 100 | if (action === 'submit') return this.submitForm() 101 | if (action === 'redirect') return this.redirectUrl() 102 | return false 103 | }, 104 | prepare3dsData() { 105 | const params = {} 106 | const data = this.attr('submit3ds') 107 | if (data['3ds']) { 108 | params.token = this.attr('token') 109 | params.uid = this.getUID() 110 | params.frame = true 111 | if (data['send_data'].TermUrl) { 112 | data['send_data'].TermUrl = [ 113 | data['send_data'].TermUrl, 114 | this.utils.param(params), 115 | ].join('#!!') 116 | } 117 | } 118 | return data 119 | }, 120 | waitOn3dsDecline() { 121 | const data = this.alt('submit3ds.checkout_data', { 122 | js_wait_on_3ds_decline: false, 123 | js_wait_on_3ds_decline_duration: 0, 124 | }) 125 | return data.js_wait_on_3ds_decline 126 | ? data.js_wait_on_3ds_decline_duration 127 | : 0 128 | }, 129 | submit3dsForm() { 130 | if (this.attr('submit3ds.checkout_data')) { 131 | this.connector.trigger('modal', this.prepare3dsData()) 132 | } 133 | }, 134 | supportedMethod(method) { 135 | const item = this.find('methods', function (item) { 136 | return item.alt('supportedMethods', '').match(method) 137 | }) 138 | if (item) { 139 | this.attr('methods', [item.serialize()]) 140 | } else { 141 | this.attr('methods', []) 142 | } 143 | }, 144 | }) 145 | -------------------------------------------------------------------------------- /src/core/template.js: -------------------------------------------------------------------------------- 1 | const { ClassObject } = require('./class') 2 | const Views = require('../views') 3 | 4 | function empty(name) { 5 | return function (data) { 6 | return ['template', name, 'not found'].join(' ') 7 | } 8 | } 9 | 10 | exports.Template = ClassObject.extend({ 11 | init(name) { 12 | this.view = Views[name] || empty(name) 13 | }, 14 | render(data) { 15 | return this.view(data) 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /src/core/utils.js: -------------------------------------------------------------------------------- 1 | const digitTest = /^\d+$/; 2 | const keyBreaker = /([^\[\]]+)|(\[\])/g; 3 | const paramTest = /([^?#]*)(#.*)?$/; 4 | const r20 = /%20/g; 5 | const rbracket = /\[\]$/; 6 | const matchSymbol = /\s([a-zA-Z]+)/ 7 | 8 | const prep = (str) => { 9 | return decodeURIComponent(str.replace(/\+/g, ' ')); 10 | } 11 | const flatten = (array) => { 12 | let result = [] 13 | forEach(array, (item) => { 14 | result = result.concat(isArray(item) ? flatten(item) : item) 15 | }) 16 | return result 17 | } 18 | 19 | const recursiveParams = (prefix, obj, next) => { 20 | if (isArray(obj)) { 21 | forEach(obj, (item, prop) => { 22 | if (rbracket.test(prefix)) { 23 | next(prefix, item); 24 | } else { 25 | recursiveParams(prefix + "[" + (isPlainObject(obj) ? prop : "") + "]", item, next); 26 | } 27 | }); 28 | } else if (isPlainObject(obj)) { 29 | forEach(obj, (item, prop) => { 30 | recursiveParams(prefix + "[" + prop + "]", item, next); 31 | }); 32 | } else { 33 | next(prefix, obj); 34 | } 35 | } 36 | 37 | const getType = exports.getType = (o) => { 38 | return ({}).toString.call(o).match(matchSymbol)[1].toLowerCase(); 39 | } 40 | const isPlainObject = exports.isPlainObject = (o) => { 41 | return (!!o && typeof o === 'object' && o.constructor === Object); 42 | } 43 | 44 | const isFunction = exports.isFunction = (o) => { 45 | return getType(o) === 'function' 46 | } 47 | 48 | const isArray = exports.isArray = (o) => { 49 | return getType(o) === 'array'; 50 | } 51 | 52 | const isElement = exports.isElement = (o) => { 53 | return o && o.nodeType === 1; 54 | } 55 | 56 | const toArray = exports.toArray = (o) => { 57 | return [].slice.call(o); 58 | } 59 | 60 | const hasProp = exports.hasProp = (o, v) => { 61 | return o && o.hasOwnProperty(v); 62 | } 63 | 64 | const forEach = exports.forEach = (ob, cb, cx) => { 65 | for (let p in ob) 66 | if (hasProp(ob, p)) 67 | cb.call(cx || null, ob[p], p); 68 | } 69 | 70 | const cleanObject = exports.cleanObject = (ob) => { 71 | for (let p in ob) { 72 | if (hasProp(ob, p)) { 73 | if (ob[p].length === 0) { 74 | if (isArray(ob)) ob.splice(p, 1); 75 | if (isPlainObject(ob)) delete ob[p]; 76 | } else if (isPlainObject(ob[p])) { 77 | cleanObject(ob[p]); 78 | } 79 | } 80 | } 81 | return ob; 82 | } 83 | 84 | 85 | const extend = exports.extend = (obj, ...args) => { 86 | forEach(args, function (o) { 87 | if (o !== null) { 88 | forEach(o, function (value, key) { 89 | if (isPlainObject(value)) { 90 | obj[key] = extend(obj[key] || {}, value); 91 | } else { 92 | obj[key] = value; 93 | } 94 | }); 95 | } 96 | }); 97 | return obj; 98 | } 99 | 100 | const uuid = exports.uuid = () => { 101 | let a = 0, b = ''; 102 | while (a++ < 36) { 103 | if (a * 51 & 52) { 104 | b += (a ^ 15 ? 8 ^ Math.random() * (a ^ 20 ? 16 : 4) : 4).toString(16); 105 | } else { 106 | b += '-'; 107 | } 108 | } 109 | return b; 110 | } 111 | 112 | exports.isObject = (o) => { 113 | return getType(o) === 'object'; 114 | } 115 | 116 | exports.isRegexp = (o) => { 117 | return getType(o) === 'regexp'; 118 | } 119 | 120 | exports.isArguments = (o) => { 121 | return getType(o) === 'arguments'; 122 | } 123 | 124 | exports.isError = (o) => { 125 | return getType(o) === 'error'; 126 | } 127 | 128 | exports.isDate = (o) => { 129 | return getType(o) === 'date'; 130 | } 131 | 132 | exports.isString = (o) => { 133 | return getType(o) === 'string'; 134 | } 135 | 136 | exports.isNumber = (o) => { 137 | return getType(o) === 'number'; 138 | } 139 | 140 | exports.map = (ob, cb, cx) => { 141 | let p, t, r = []; 142 | for (p in ob) 143 | if (hasProp(ob, p)) 144 | if ((t = cb.call(cx || null, ob[p], p)) !== undefined) 145 | r[p] = t; 146 | return r; 147 | } 148 | 149 | exports.querySelectorAll = (o, p) => { 150 | return toArray((p || document).querySelectorAll(o)) 151 | } 152 | 153 | exports.querySelector = (o, p) => { 154 | return (p || document).querySelector(o) 155 | } 156 | 157 | 158 | exports.deparam = (params) => { 159 | const data = {} 160 | if (params.charAt(0) === '?') params = params.slice(1) 161 | if (params && paramTest.test(params)) { 162 | forEach(params.split('&'), (pair) => { 163 | let parts = pair.split('='), 164 | key = prep(parts.shift()), 165 | value = prep(parts.join('=')), 166 | current = data; 167 | if (key) { 168 | parts = key.match(keyBreaker); 169 | for (let j = 0, l = parts.length - 1; j < l; j++) { 170 | if (!current[parts[j]]) { 171 | current[parts[j]] = digitTest.test(parts[j + 1]) || parts[j + 1] === '[]' ? [] : {}; 172 | } 173 | current = current[parts[j]]; 174 | } 175 | const lastPart = parts.pop(); 176 | if (lastPart === '[]') { 177 | current.push(value); 178 | } else { 179 | current[lastPart] = value; 180 | } 181 | } 182 | }); 183 | } 184 | return data; 185 | } 186 | 187 | exports.param = (obj) => { 188 | const result = []; 189 | forEach(obj, (item, prop) => { 190 | recursiveParams(prop, item, (key, value) => { 191 | value = value == null ? "" : value; 192 | result.push([encodeURIComponent(key), encodeURIComponent(value)].join('=')); 193 | }); 194 | }); 195 | return result.join("&").replace(r20, '+'); 196 | } 197 | 198 | exports.removeElement = (el) => { 199 | el.parentNode.removeChild(el); 200 | } 201 | 202 | exports.createElement = (tag) => { 203 | return document.createElement(tag); 204 | } 205 | 206 | exports.addClass = (...args) => { 207 | const el = args.shift() 208 | if (isElement(el) === false) return; 209 | const classList = el.className.trim().split(/\s+/) 210 | const tokens = flatten(args.map(item => item.trim().split(/\s+/))); 211 | tokens.forEach(token => { 212 | if (token && !~classList.indexOf(token)) { 213 | classList.push(token) 214 | } 215 | }) 216 | el.className = classList.join(' ').trim() 217 | } 218 | 219 | exports.removeClass = (...args) => { 220 | const el = args.shift() 221 | if (isElement(el) === false) return; 222 | const classList = el.className.trim().split(/\s+/) 223 | const tokens = flatten(args.map(item => item.trim().split(/\s+/))); 224 | tokens.forEach((token) => { 225 | if (token) { 226 | const index = classList.indexOf(token) 227 | if (!!~index) { 228 | classList.splice(index, 1) 229 | } 230 | } 231 | }) 232 | el.className = classList.join(' ').trim() 233 | } 234 | 235 | exports.removeAttr = (el, attrs) => { 236 | if (isElement(el) === false) return false; 237 | if (isPlainObject(attrs)) { 238 | forEach(attrs, (value) => { 239 | el.removeAttribute(value) 240 | }) 241 | } 242 | } 243 | 244 | exports.addAttr = (el, attrs) => { 245 | if (isElement(el) === false) return false; 246 | if (isPlainObject(attrs)) { 247 | forEach(attrs, (value, name) => { 248 | el.setAttribute(name, value) 249 | }) 250 | } 251 | } 252 | 253 | exports.getStyle = (el, prop, getComputedStyle) => { 254 | getComputedStyle = window.getComputedStyle; 255 | return (getComputedStyle ? getComputedStyle(el) : el['currentStyle'])[prop.replace(/-(\w)/gi, function (word, letter) { 256 | return letter.toUpperCase() 257 | })]; 258 | } 259 | 260 | 261 | exports.getPath = (path) => { 262 | const props = path.split('.'); 263 | const first = props.shift(); 264 | let value = null; 265 | if (hasProp(window, first)) { 266 | value = window[first]; 267 | forEach(props, (name) => { 268 | value = hasProp(value, name) ? value[name] : null; 269 | }) 270 | } 271 | return value; 272 | } 273 | 274 | exports.stringFormat = (format, params, expr) => { 275 | return (format || '').replace(expr || /{(.+?)}/g, (match, prop) => { 276 | return params[prop] || match; 277 | }); 278 | } 279 | 280 | exports.cssUnit = (value, unit) => { 281 | return String(value || 0).concat(unit || '').concat(' !important') 282 | } 283 | /** 284 | * @function 285 | * @type {function(*, *, *)} 286 | */ 287 | exports.getPaymentRequest = ((cx) => { 288 | let NativePaymentRequest; 289 | if (hasProp(cx, 'PaymentRequest') && isFunction(cx.PaymentRequest)) { 290 | NativePaymentRequest = cx.PaymentRequest 291 | } 292 | return (methods, details, options) => { 293 | let request = null; 294 | details = details || {}; 295 | details.id = uuid(); 296 | options = options || {}; 297 | if (NativePaymentRequest) { 298 | try { 299 | request = new NativePaymentRequest(methods, details, options); 300 | } catch (e) { 301 | request = null; 302 | } 303 | } 304 | return request 305 | } 306 | })(window) 307 | 308 | exports.jsonParse = (value, defaults) => { 309 | try { 310 | return JSON.parse(value) 311 | } catch (e) { 312 | return defaults 313 | } 314 | } 315 | 316 | -------------------------------------------------------------------------------- /src/core/widget/button.js: -------------------------------------------------------------------------------- 1 | const { Widget } = require('./index') 2 | 3 | exports.WidgetButton = Widget.extend({ 4 | attributes: {}, 5 | initElement(el) { 6 | if (this.utils.isPlainObject(this.params.attributes)) { 7 | this.utils.extend(this.attributes, this.params.attributes) 8 | } 9 | this.addSelectorEvent(el, 'click', 'sendRequest') 10 | }, 11 | getRequestParams(el) { 12 | return this.utils.extend( 13 | {}, 14 | this.params.options, 15 | this.getElementData(el) 16 | ) 17 | }, 18 | getElementData(el) { 19 | const result = {} 20 | this.utils.forEach(this.attributes, function (value, key) { 21 | if (el.hasAttribute(key)) { 22 | result[value] = el.getAttribute(key) 23 | } 24 | }) 25 | return result 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /src/core/widget/form.js: -------------------------------------------------------------------------------- 1 | const { Module } = require('../module') 2 | const { Widget } = require('./index') 3 | 4 | const FormData = Module.extend({ 5 | init(form) { 6 | this.setFormElement(form) 7 | }, 8 | setFormElement(form) { 9 | if (this.utils.isElement(form)) { 10 | this.form = form 11 | } 12 | }, 13 | getData(filter) { 14 | const params = this.deparam(this.serializeArray()) 15 | return filter === true ? this.utils.cleanObject(params) : params 16 | }, 17 | serializeArray() { 18 | const list = this.utils.toArray(this.form.elements) 19 | return this.utils.map(list, function (field) { 20 | if (field.disabled || field.name === '') return 21 | if (field.type.match('checkbox|radio') && !field.checked) return 22 | return { 23 | name: field.name, 24 | value: field.value, 25 | } 26 | }) 27 | }, 28 | serializeAndEncode() { 29 | return this.utils 30 | .map(this.serializeArray(), function (field) { 31 | return [field.name, encodeURIComponent(field.value)].join('=') 32 | }) 33 | .join('&') 34 | }, 35 | deparam(obj) { 36 | let prop, 37 | result = {} 38 | const breaker = /[^\[\]]+|\[\]$/g 39 | const attr = function (name, value) { 40 | let i, 41 | data = result, 42 | last = name.pop(), 43 | len = name.length 44 | for (i = 0; i < len; i++) { 45 | if (!data[name[i]]) 46 | data[name[i]] = len === i + 1 && last === '[]' ? [] : {} 47 | data = data[name[i]] 48 | } 49 | if (last === '[]') { 50 | data.push(value) 51 | } else { 52 | data[last] = value 53 | } 54 | } 55 | for (prop in obj) { 56 | if (obj.hasOwnProperty(prop)) { 57 | attr(obj[prop].name.match(breaker), obj[prop].value) 58 | } 59 | } 60 | return result 61 | }, 62 | }) 63 | 64 | exports.WidgetForm = Widget.extend({ 65 | initElement(el) { 66 | this.addSelectorEvent(el, 'submit', 'sendRequest') 67 | }, 68 | getRequestParams(el) { 69 | return this.utils.extend( 70 | {}, 71 | this.params.options, 72 | new FormData(el).getData() 73 | ) 74 | }, 75 | }) 76 | -------------------------------------------------------------------------------- /src/core/widget/index.js: -------------------------------------------------------------------------------- 1 | const { Api } = require('../api') 2 | 3 | exports.Widget = Api.extend({ 4 | init(params) { 5 | this.initParams(params) 6 | this.initWidget() 7 | }, 8 | initWidget() { 9 | this.initOptions(this.params.options) 10 | if (this.utils.isString(this.params.element)) { 11 | this.initElement(this.params.element) 12 | } 13 | }, 14 | initOptions() { 15 | if (this.utils.isPlainObject(this.params.options)) { 16 | this.params.options = this.params.options || {} 17 | } 18 | }, 19 | initElement(el) {}, 20 | addSelectorEvent(el, ev, cb) { 21 | this.each(this.utils.querySelectorAll(el), function (cx, element) { 22 | this.addEvent(element, ev, cb) 23 | }) 24 | return this 25 | }, 26 | getRequestParams() { 27 | return {} 28 | }, 29 | sendRequest(el, ev) { 30 | if (ev.defaultPrevented) return 31 | ev.preventDefault() 32 | this.trigger('request', this.getRequestParams(el)) 33 | this.scope(function () { 34 | this.request( 35 | 'api.checkout.form', 36 | 'request', 37 | this.getRequestParams(el) 38 | ) 39 | .done(this.proxy('onSuccess')) 40 | .fail(this.proxy('onError')) 41 | .progress(this.proxy('onProgress')) 42 | }) 43 | }, 44 | onProgress(cx, model) { 45 | this.trigger('progress', model) 46 | }, 47 | onSuccess(cx, model) { 48 | model.sendResponse() 49 | model.submitToMerchant() 50 | this.trigger('success', model) 51 | }, 52 | onError(cx, model) { 53 | this.trigger('error', model) 54 | }, 55 | }) 56 | -------------------------------------------------------------------------------- /src/views/index.js: -------------------------------------------------------------------------------- 1 | exports.acs = require('./template/acs') -------------------------------------------------------------------------------- /src/views/template/acs.js: -------------------------------------------------------------------------------- 1 | const styles = require('./acs_styles') 2 | 3 | module.exports = (data) => ` 4 | ${styles(data)} 5 |
6 |
7 |
8 | 9 |
10 | ${data.messages.modalHeader} 11 | ${data.messages.modalLinkLabel} 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 | ` -------------------------------------------------------------------------------- /src/views/template/acs_styles.js: -------------------------------------------------------------------------------- 1 | module.exports = (data) => ` 2 | 94 | ` -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @var $checkout = require('./dist/checkout') 3 | */ 4 | 5 | QUnit.module('checkout',{ 6 | before:function(){ 7 | this.checkout = $checkout('Api'); 8 | this.checkout.setOrigin('https://api.dev.fondy.eu'); 9 | }, 10 | beforeEach:function(){ 11 | this.params = { 12 | merchant_id:'1396424', 13 | currency:'UAH', 14 | amount:0 15 | }; 16 | } 17 | }); 18 | 19 | QUnit.test('api.checkout.form.request:card', function( assert ) { 20 | var params = {}; 21 | var done = assert.async(); 22 | QUnit.extend(params,this.params); 23 | QUnit.extend(params,{ 24 | payment_system:'card', 25 | card_number: '4444555566661111', 26 | expiry_date: '12/30', 27 | cvv2: '111', 28 | email:'test@example.com' 29 | }); 30 | this.checkout.scope(function(){ 31 | this.request('api.checkout.form','request',params).done(function(){ 32 | assert.ok(true,'success result'); 33 | done(); 34 | }).fail(function(model){ 35 | assert.ok(model.attr('error.code'),'error code exist'); 36 | assert.ok(model.attr('error.message'),'error message exist'); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | 42 | 43 | 44 | --------------------------------------------------------------------------------