├── README.md ├── api ├── .firebaserc ├── firebase.json ├── database.rules.json └── functions │ ├── config.sample.json │ ├── package.json │ ├── line-helper.js │ └── index.js ├── web ├── .firebaserc ├── public │ ├── favicon.ico │ ├── images │ │ ├── lp.png │ │ └── check.png │ ├── manifest.json │ └── index.html ├── src │ ├── assets │ │ └── img │ │ │ └── man.png │ ├── utils │ │ ├── vConsole.js │ │ ├── liffHelper.js │ │ └── messagingApiHelper.js │ ├── App.test.js │ ├── index.js │ ├── App.css │ ├── App.js │ ├── pages │ │ ├── Thankyou.js │ │ └── LINEPay.js │ └── registerServiceWorker.js ├── firebase.json ├── package.json ├── .gitignore └── README.md └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | # LINE API DEMO -------------------------------------------------------------------------------- /api/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "sitthi-linepay-demo" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /api/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "sitthi-linepay-demo" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /api/database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": true, 4 | ".write": true 5 | } 6 | } -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamnan43/linepay-demo/HEAD/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/images/lp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamnan43/linepay-demo/HEAD/web/public/images/lp.png -------------------------------------------------------------------------------- /web/public/images/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamnan43/linepay-demo/HEAD/web/public/images/check.png -------------------------------------------------------------------------------- /web/src/assets/img/man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamnan43/linepay-demo/HEAD/web/src/assets/img/man.png -------------------------------------------------------------------------------- /web/src/utils/vConsole.js: -------------------------------------------------------------------------------- 1 | var VConsole = require('vconsole/dist/vconsole.min.js'); 2 | export default new VConsole() ; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | api/functions/config.json 2 | api/functions/firebase-admin-key.json 3 | web/src/config.json 4 | api/functions/node_modules 5 | web/node_modules -------------------------------------------------------------------------------- /web/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /web/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /web/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import registerServiceWorker from './registerServiceWorker'; 5 | import 'bootstrap/dist/css/bootstrap.css'; 6 | import 'bootstrap/dist/css/bootstrap-theme.css'; 7 | 8 | ReactDOM.render(, document.getElementById('root')); 9 | registerServiceWorker(); 10 | -------------------------------------------------------------------------------- /web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /web/src/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | text-align: center; 3 | } 4 | 5 | .app-header { 6 | background-color: #00C300; 7 | height: 50px; 8 | padding: 5px; 9 | color: white; 10 | } 11 | 12 | .app-title { 13 | font-size: 1.5em; 14 | } 15 | 16 | .avatar-img { 17 | border-radius: 50%; 18 | } 19 | 20 | .page-content { 21 | padding: 5px; 22 | } 23 | 24 | .message-label { 25 | text-align: left; 26 | display: block; 27 | } -------------------------------------------------------------------------------- /api/functions/config.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "line": { 3 | "channelAccessToken": "", 4 | "channelSecret": "" 5 | }, 6 | "linepay": { 7 | "api": "https://sandbox-api-pay.line.me", 8 | "channelId": "", 9 | "channelSecret": "" 10 | }, 11 | "firebase": { 12 | "apiKey": "", 13 | "authDomain": "", 14 | "databaseURL": "", 15 | "projectId": "", 16 | "storageBucket": "", 17 | "serviceAccountFile": "./firebase-admin-key.json" 18 | }, 19 | "apiUrl": "", 20 | "webUrl": "" 21 | } -------------------------------------------------------------------------------- /api/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "serve": "firebase serve --only functions", 6 | "shell": "firebase functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "dependencies": { 12 | "@line/bot-sdk": "^6.7.1", 13 | "cors": "^2.8.4", 14 | "firebase-admin": "^6.0.0", 15 | "firebase-functions": "^2.0.3", 16 | "moment": "^2.22.2", 17 | "request": "^2.88.0", 18 | "request-promise": "^4.2.2" 19 | }, 20 | "private": true 21 | } 22 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-liff-boilerplate", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "bootstrap": "^3.3.7", 7 | "moment": "^2.24.0", 8 | "react": "^16.4.1", 9 | "react-bootstrap": "^0.32.1", 10 | "react-dom": "^16.4.1", 11 | "react-geolocated": "^2.4.0", 12 | "react-router-dom": "^4.3.1", 13 | "react-scripts": "1.1.4", 14 | "request-promise": "^4.2.4", 15 | "sweetalert2": "^7.26.9", 16 | "vconsole": "^3.3.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject", 23 | "ngrok": "./ngrok http 3000" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { BrowserRouter as Router, Route } from "react-router-dom"; 3 | import './App.css'; 4 | import LINEPay from './pages/LINEPay'; 5 | import Thankyou from './pages/Thankyou'; 6 | 7 | class App extends Component { 8 | render() { 9 | return ( 10 |
11 |
12 |

Rabbit LINE Pay Demo

13 |
14 | 15 |
16 | 17 | 18 |
19 |
20 |
21 | ); 22 | } 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | 9 | # Firebase cache 10 | .firebase/ 11 | 12 | # Firebase config 13 | 14 | # Uncomment this if you'd like others to create their own Firebase project. 15 | # For a team working on the same Firebase project(s), it is recommended to leave 16 | # it commented so all members can deploy to the same project(s) in .firebaserc. 17 | # .firebaserc 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (http://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | 67 | build -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | Rabbit LINE Pay Demo 23 | 24 | 25 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /web/src/pages/Thankyou.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import liffHelper from '../utils/liffHelper'; 3 | import rp from 'request-promise'; 4 | import qs from 'query-string'; 5 | 6 | export default class Thankyou extends Component { 7 | async componentDidMount() { 8 | try { 9 | const { transactionId } = qs.parse(window.location.search); 10 | console.log('transactionId', transactionId); 11 | this.confirmPaymentRequest(transactionId); 12 | } catch (err) { 13 | console.log(err); 14 | } 15 | } 16 | 17 | confirmPaymentRequest(transactionId) { 18 | return new Promise((resolve, reject) => { 19 | return rp({ 20 | method: 'GET', 21 | uri: `https://us-central1-sitthi-linepay-demo.cloudfunctions.net/confirmPaymentClient?transactionId=${transactionId}`, 22 | json: true, 23 | }).then((response) => { 24 | console.log('response', response); 25 | }).catch(function (err) { 26 | console.log('err', err); 27 | reject(err); 28 | }); 29 | }); 30 | } 31 | 32 | render() { 33 | return ( 34 |
35 |
36 |
37 |
38 | checked 39 |

ได้รับชำระเงินเรียบร้อยแล้ว

40 |

ขอบคุณค่ะ

41 |
42 |
43 | 44 |
45 |
46 |
47 | ); 48 | } 49 | } -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # react-liff-boilerplate 2 | Starter Kit for ReactJs developer to run and learn [LINE Frontend Framework](https://developers.line.me/en/docs/liff/overview/) (LIFF) 3 | 4 | ## How it work 5 | This project build from [create-react-app](https://github.com/facebook/create-react-app) and [bootstrap](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-bootstrap). Then add Line Frontend Framework (LIFF) [SDK](https://developers.line.me/en/docs/liff/developing-liff-apps/) and also provide `LiffHelper` and `messagingApiHelper` to help in communicate with LINE LIFF Platform. 6 | 7 | # Install 8 | Clone and run 9 | ``` 10 | npm install 11 | ``` 12 | Create config file in JSON and named it as `config.json` 13 | ```json 14 | { 15 | "line" : { 16 | "channelAccessToken": "YOUR_CHANNEL_ACCESS_TOKEN" 17 | } 18 | } 19 | ``` 20 | Run 21 | ``` 22 | npm start 23 | ``` 24 | then you can access [http://localhost:3000](http://localhost:3000) 25 | 26 | ## Demo 27 | **Profile** : [http://localhost:3000](http://localhost:3000) 28 | 29 | **SendMessage** : [http://localhost:3000/message](http://localhost:3000/message) 30 | 31 | **LIFF Window** : [http://localhost:3000/window](http://localhost:3000/window) 32 | 33 | 34 | # API 35 | ## Get Profile 36 | Utility class `liffHelper` automatic initial LIFF when application launched. You can get user profile with command 37 | ```js 38 | liffHelper.getProfile() 39 | .then(profile => { 40 | // do something 41 | }); 42 | ``` 43 | ## Get LIFF Info 44 | Get User Context information 45 | ``` 46 | liffHelper.getLIFFInfo(); 47 | ``` 48 | return [result](https://developers.line.me/en/reference/liff/#liffinit()) 49 | 50 | ## Send Message 51 | ``` 52 | const message = messagingApiHelper.createTextMessage('text'); 53 | liffHelper.sendMessages(message); 54 | ``` 55 | ## LIFF Window 56 | ``` 57 | liffHelper.openWindow('https://url.com', false); // open url in LINE browser 58 | liffHelper.openWindow('https://url.com', true); // open url in external browser 59 | liffHelper.closeWindow(); // close LIFF window 60 | ``` 61 | 62 | ## Author 63 | Sitthi Thiammekha 64 | -------------------------------------------------------------------------------- /api/functions/line-helper.js: -------------------------------------------------------------------------------- 1 | const maxCarouselColumns = 10; 2 | 3 | function createTextMessage(text) { 4 | return { 5 | type: 'text', 6 | text: text 7 | }; 8 | } 9 | 10 | function createImageMessage(originalContentUrl, previewImageUrl) { 11 | return { 12 | type: 'image', 13 | originalContentUrl: originalContentUrl, 14 | previewImageUrl 15 | }; 16 | } 17 | 18 | function createButtonMessage(title, actions) { 19 | return { 20 | type: 'template', 21 | altText: title, 22 | template: { 23 | type: 'buttons', 24 | text: title.substring(0, 60), 25 | actions: actions, 26 | }, 27 | }; 28 | } 29 | 30 | function createConfirmMessage(title, actions) { 31 | return { 32 | type: 'template', 33 | altText: title, 34 | template: { 35 | type: 'confirm', 36 | text: title, 37 | actions: actions, 38 | }, 39 | }; 40 | } 41 | 42 | function createCarouselMessage(title, columns) { 43 | return { 44 | type: 'template', 45 | altText: title, 46 | template: { 47 | type: 'carousel', 48 | columns: columns, 49 | }, 50 | }; 51 | } 52 | 53 | function createImageCarouselMessage(title, columns) { 54 | return { 55 | type: 'template', 56 | altText: title, 57 | template: { 58 | type: 'image_carousel', 59 | columns: columns, 60 | }, 61 | }; 62 | } 63 | 64 | function createFlexMessage(title, containers) { 65 | return { 66 | "type": "flex", 67 | "altText": title, 68 | "contents": containers 69 | }; 70 | } 71 | 72 | function createFlexCarouselMessage(title, containers) { 73 | return createFlexMessage(title, { 74 | "type": "carousel", 75 | "contents": containers, 76 | }); 77 | } 78 | 79 | module.exports = { 80 | createTextMessage: createTextMessage, 81 | createImageMessage: createImageMessage, 82 | createButtonMessage: createButtonMessage, 83 | createConfirmMessage: createConfirmMessage, 84 | createCarouselMessage: createCarouselMessage, 85 | createImageCarouselMessage: createImageCarouselMessage, 86 | createFlexMessage: createFlexMessage, 87 | createFlexCarouselMessage: createFlexCarouselMessage, 88 | maxCarouselColumns: maxCarouselColumns 89 | }; -------------------------------------------------------------------------------- /web/src/utils/liffHelper.js: -------------------------------------------------------------------------------- 1 | const liff = window.liff; 2 | let isInit = false; 3 | let profile = {}; 4 | let liffInfo = {}; 5 | 6 | class liffHelper { 7 | init() { 8 | return new Promise((resolve, reject) => { 9 | if (!isInit) { 10 | liff.init( 11 | data => { 12 | liffInfo = data; 13 | isInit = true; 14 | resolve(); 15 | }, 16 | err => { 17 | console.log('Fail to init LIFF, please run inside LINE only'); 18 | reject(); 19 | } 20 | ); 21 | } else { 22 | resolve(); 23 | } 24 | }); 25 | } 26 | 27 | getLIFFInfo() { 28 | return liffInfo; 29 | } 30 | 31 | getProfile() { 32 | return new Promise((resolve, reject) => { 33 | this.init() 34 | .then(() => { 35 | if (isInit && !profile.userId) { 36 | liff.getProfile() 37 | .then(pf => { 38 | profile = pf; 39 | resolve(profile); 40 | }) 41 | .catch((err) => { 42 | console.log('get profile error', err); 43 | profile = { 44 | userId: 'U4f9d55b31f313f297c4a1e08283d6456', 45 | }; 46 | resolve(profile); 47 | // reject(err); 48 | }); 49 | } else { 50 | resolve(profile) 51 | } 52 | }) 53 | .catch(err => { reject(err) }); 54 | }); 55 | } 56 | 57 | closeWindow() { 58 | liff.closeWindow(); 59 | } 60 | 61 | openWindow(url, external) { 62 | liff.openWindow({ url, external }); 63 | } 64 | 65 | sendMessages(messages) { 66 | const messagesToSend = Array.isArray(messages) ? messages : [messages]; 67 | return new Promise((resolve, reject) => { 68 | this.init() 69 | .then(() => { 70 | liff.sendMessages(messagesToSend) 71 | .then(() => { 72 | resolve(); 73 | }) 74 | .catch((err) => { 75 | reject(err); 76 | }); 77 | }) 78 | .catch((err) => { 79 | reject(err); 80 | }); 81 | }); 82 | } 83 | }; 84 | export default new liffHelper(); -------------------------------------------------------------------------------- /web/src/utils/messagingApiHelper.js: -------------------------------------------------------------------------------- 1 | function createTextMessage(text) { 2 | return { 3 | type: 'text', 4 | text: text 5 | }; 6 | } 7 | 8 | function createImageMessage(originalContentUrl, previewImageUrl) { 9 | return { 10 | type: 'image', 11 | originalContentUrl: originalContentUrl, 12 | previewImageUrl 13 | }; 14 | } 15 | 16 | function createVDOMessage(originalContentUrl, previewImageUrl) { 17 | return { 18 | type: 'video', 19 | originalContentUrl: originalContentUrl, 20 | previewImageUrl: previewImageUrl 21 | }; 22 | } 23 | 24 | function createAudioMessage(originalContentUrl, duration) { 25 | return { 26 | type: 'audio', 27 | originalContentUrl: originalContentUrl, 28 | duration: duration 29 | }; 30 | } 31 | 32 | function createButtonMessage(title, actions) { 33 | return { 34 | type: 'template', 35 | altText: title, 36 | template: { 37 | type: 'buttons', 38 | text: title.substring(0, 60), 39 | actions: actions, 40 | }, 41 | }; 42 | } 43 | 44 | function createButtonMessageWithImage(title, text, imageUrl, actions) { 45 | return { 46 | type: 'template', 47 | altText: title, 48 | template: { 49 | type: 'buttons', 50 | thumbnailImageUrl: imageUrl, 51 | title: title.substring(0, 40), 52 | text: text.substring(0, 60), 53 | // defaultAction: actions.getImageAction(extra), 54 | actions: actions, 55 | }, 56 | }; 57 | } 58 | 59 | function createLocationMessage(latitude, longitude) { 60 | return { 61 | type: 'location', 62 | title: 'my location', 63 | address: `@ ${latitude}:${longitude}`, 64 | latitude: latitude, 65 | longitude: longitude 66 | } 67 | } 68 | 69 | function createConfirmMessage(title, actions) { 70 | return { 71 | type: 'template', 72 | altText: title, 73 | template: { 74 | type: 'confirm', 75 | text: title, 76 | actions: actions, 77 | }, 78 | }; 79 | } 80 | 81 | function createCarouselMessage(title, columns) { 82 | return { 83 | type: 'template', 84 | altText: title, 85 | template: { 86 | type: 'carousel', 87 | columns: columns, 88 | }, 89 | }; 90 | } 91 | 92 | // function createCarouselColumns(title, text, imageUrl, extra, isFriend) { 93 | // let columnOptions = options.getCandidateProfileAction(extra, isFriend); 94 | // return { 95 | // thumbnailImageUrl: imageUrl, 96 | // title: title.substring(0, 40), 97 | // text: text.substring(0, 60), 98 | // defaultAction: options.getImageAction(extra), 99 | // actions: columnOptions 100 | 101 | // }; 102 | // } 103 | 104 | function createImageCarouselMessage(title, columns) { 105 | return { 106 | type: 'template', 107 | altText: title, 108 | template: { 109 | type: 'image_carousel', 110 | columns: columns, 111 | }, 112 | }; 113 | } 114 | 115 | // function createImageCarouselColumns(actionText, imageUrl, extra) { 116 | // return { 117 | // imageUrl: imageUrl, 118 | // action: options.getCandidateImageAction(actionText, extra) 119 | // }; 120 | // } 121 | 122 | function createFlexMessage(title, containers) { 123 | return { 124 | "type": "flex", 125 | "altText": title, 126 | "contents": containers 127 | }; 128 | } 129 | 130 | function createFlexCarouselMessage(title, containers) { 131 | return createFlexMessage(title, { 132 | "type": "carousel", 133 | "contents": containers, 134 | }); 135 | } 136 | 137 | module.exports = { 138 | createTextMessage: createTextMessage, 139 | createImageMessage: createImageMessage, 140 | createVDOMessage: createVDOMessage, 141 | createAudioMessage: createAudioMessage, 142 | createButtonMessage: createButtonMessage, 143 | createButtonMessageWithImage: createButtonMessageWithImage, 144 | createConfirmMessage: createConfirmMessage, 145 | createLocationMessage: createLocationMessage, 146 | createCarouselMessage: createCarouselMessage, 147 | // createCarouselColumns: createCarouselColumns, 148 | createImageCarouselMessage: createImageCarouselMessage, 149 | // createImageCarouselColumns: createImageCarouselColumns, 150 | createFlexMessage: createFlexMessage, 151 | createFlexCarouselMessage: createFlexCarouselMessage, 152 | // maxCarouselColumns: maxCarouselColumns 153 | }; -------------------------------------------------------------------------------- /web/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /web/src/pages/LINEPay.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import liffHelper from '../utils/liffHelper'; 3 | import messagingApiHelper from '../utils/messagingApiHelper'; 4 | import rp from 'request-promise'; 5 | import moment from 'moment'; 6 | // import '../utils/vConsole'; 7 | 8 | export default class Profile extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.orders = [ 12 | { 13 | name: 'ข้าวมันไก่', 14 | qty: 2, 15 | price: 45, 16 | }, 17 | { 18 | name: 'ข้าวขาหมู', 19 | qty: 1, 20 | price: 45, 21 | }, 22 | { 23 | name: 'น้ำแข็ง', 24 | qty: 3, 25 | price: 1, 26 | } 27 | ]; 28 | this.state = { 29 | lineProfile: {}, 30 | } 31 | liffHelper.getProfile() 32 | .then(profile => { 33 | this.setState({ lineProfile: profile }); 34 | }); 35 | } 36 | 37 | createOrderMessage(orders) { 38 | let total = orders.reduce((total, item) => { return total + (item.qty * item.price) }, 0); 39 | let text = `รายการที่สั่ง\n`; 40 | text += 'ร้าน LINEPAY DEMO SHOP\n' 41 | text += `${moment().format('DD/MM/YYYY HH:mm')}\n` 42 | text += '----------\n' 43 | orders.forEach(order => { 44 | text += `${order.name} [${order.qty}] x ฿${order.price}\n` 45 | }); 46 | text += '----------\n' 47 | text += `รวมทั้งหมด ${total} บาท`; 48 | return messagingApiHelper.createTextMessage(text); 49 | } 50 | 51 | sendOrderSummary() { 52 | return new Promise((resolve, reject) => { 53 | if (this.state.lineProfile.userId) { 54 | let orders = this.orders; 55 | let message = [this.createOrderMessage(orders)]; 56 | return liffHelper.sendMessages(message); 57 | } else { 58 | resolve(); 59 | } 60 | }); 61 | } 62 | 63 | startLinePayment(method, payType) { 64 | return this.makeLinePaymentRequest(method, payType) 65 | .then(() => { 66 | this.sendOrderSummary(); 67 | }).then(() => { 68 | liffHelper.closeWindow(); 69 | }); 70 | } 71 | 72 | makeLinePaymentRequest(method, payType) { 73 | return new Promise((resolve, reject) => { 74 | const total = this.orders.reduce((a, b) => (a + (b.qty * b.price)), 0); 75 | let userId = this.state.lineProfile.userId; 76 | let body = { 77 | productName: 'LINEPAY DEMO SHOP', 78 | amount: total, 79 | orderId: moment().format('x'), 80 | orders: this.orders, 81 | userId, 82 | payType, 83 | }; 84 | 85 | return rp({ 86 | method: 'POST', 87 | uri: `https://us-central1-sitthi-linepay-demo.cloudfunctions.net/reservePayment${method}`, 88 | body: body, 89 | json: true, 90 | }).then((response) => { 91 | console.log('response', response); 92 | if (response.returnCode === '0000') { 93 | const paymentUrl = response.info.paymentUrl.web; 94 | liffHelper.openWindow(paymentUrl, false); 95 | resolve(response); 96 | } else { 97 | console.log('response.returnCode', response.returnCode); 98 | reject(new Error(response.returnCode)); 99 | } 100 | }).catch(function (err) { 101 | console.log('err', err); 102 | reject(err); 103 | }); 104 | }); 105 | } 106 | 107 | render() { 108 | return ( 109 |
110 |
111 |
112 |
113 |

รายการที่สั่ง

114 |
115 |
รายการ
116 |
ราคา
117 |
รวม
118 |
119 |
120 |
121 |
ข้าวมันไก่ (2)
122 |
45
123 |
90 บาท
124 |
125 |
126 |
ข้าวขาหมู (1)
127 |
45
128 |
45 บาท
129 |
130 |
131 |
น้ำแข็ง (3)
132 |
1
133 |
3 บาท
134 |
135 |
136 |
137 |
รวม 3 รายการ
138 |
-
139 |
138 บาท
140 |
141 |
142 |
143 | ชำระเงิน 144 |
145 |
146 |
 
147 |
148 | 153 | Server Mode 154 |
155 |
156 | 161 | Client Mode 162 |
163 |
 
164 |
165 |
166 |
167 |
168 |
169 |
170 | ); 171 | } 172 | } -------------------------------------------------------------------------------- /api/functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const cors = require('cors')({ origin: true }); 3 | const rp = require('request-promise'); 4 | const config = require('./config.json'); 5 | const firebase = require("firebase-admin"); 6 | const lineHelper = require('./line-helper'); 7 | const lineSdk = require('@line/bot-sdk'); 8 | const line = new lineSdk.Client(config.line); 9 | var firebaseConfig = config.firebase; 10 | firebaseConfig.credential = firebase.credential.cert(require(firebaseConfig.serviceAccountFile)); 11 | firebase.initializeApp(firebaseConfig); 12 | var database = firebase.database(); 13 | 14 | exports.reservePaymentServer = functions.https.onRequest((req, res) => { 15 | return cors(req, res, () => { 16 | reservePaymentServer(req, res); 17 | }); 18 | }); 19 | 20 | exports.reservePaymentClient = functions.https.onRequest((req, res) => { 21 | return cors(req, res, () => { 22 | reservePaymentClient(req, res); 23 | }); 24 | }); 25 | 26 | exports.confirmPaymentServer = functions.https.onRequest((req, res) => { 27 | return cors(req, res, () => { 28 | confirmPaymentServer(req, res); 29 | }); 30 | }); 31 | 32 | exports.confirmPaymentClient = functions.https.onRequest((req, res) => { 33 | return cors(req, res, () => { 34 | confirmPaymentClient(req, res); 35 | }); 36 | }); 37 | 38 | function reservePaymentServer(req, res) { 39 | let { productName, amount, orderId } = req.body; 40 | let url = `${config.linepay.api}/v2/payments/request`; 41 | 42 | let payload = { 43 | productImageUrl: 'https://obs.line-scdn.net/0hnL15US6nMWMMTBu52_ZONDAJPw57YjcrdHh3BiwcbVclKSIxMCsrUn5POAAmLH5mMyt_VnlJbAEn', 44 | productName, 45 | amount, 46 | orderId, 47 | currency: 'THB', 48 | confirmUrl: `${config.apiUrl}/confirmPaymentServer`, 49 | langCd: 'th', 50 | confirmUrlType: 'SERVER', 51 | }; 52 | let headers = { 53 | 'X-LINE-ChannelId': config.linepay.channelId, 54 | 'X-LINE-ChannelSecret': config.linepay.channelSecret, 55 | 'Content-Type': 'application/json', 56 | }; 57 | rp({ 58 | method: 'POST', 59 | uri: url, 60 | body: payload, 61 | headers, 62 | json: true, 63 | }) 64 | .then(function (response) { 65 | console.log('reservePaymentServer response', JSON.stringify(response)); 66 | if (response && response.returnCode === '0000' && response.info) { 67 | const data = req.body; 68 | const transactionId = response.info.transactionId; 69 | data.transactionId = transactionId; 70 | saveTx(orderId, data); 71 | } 72 | res.send(response); 73 | }) 74 | .catch(function (err) { 75 | console.log('reservePaymentServer err', err); 76 | res.status(400).send(err); 77 | }); 78 | }; 79 | 80 | function reservePaymentClient(req, res) { 81 | let { productName, amount, orderId } = req.body; 82 | let url = `${config.linepay.api}/v2/payments/request`; 83 | 84 | let payload = { 85 | productImageUrl: 'https://obs.line-scdn.net/0hnL15US6nMWMMTBu52_ZONDAJPw57YjcrdHh3BiwcbVclKSIxMCsrUn5POAAmLH5mMyt_VnlJbAEn', 86 | productName, 87 | amount, 88 | orderId, 89 | currency: 'THB', 90 | confirmUrl: `${config.webUrl}/thankyou`, 91 | langCd: 'th', 92 | confirmUrlType: 'CLIENT', 93 | }; 94 | let headers = { 95 | 'X-LINE-ChannelId': config.linepay.channelId, 96 | 'X-LINE-ChannelSecret': config.linepay.channelSecret, 97 | 'Content-Type': 'application/json', 98 | }; 99 | rp({ 100 | method: 'POST', 101 | uri: url, 102 | body: payload, 103 | headers, 104 | json: true, 105 | }) 106 | .then(function (response) { 107 | console.log('reservePaymentClient response', JSON.stringify(response)); 108 | if (response && response.returnCode === '0000' && response.info) { 109 | const data = req.body; 110 | const transactionId = response.info.transactionId; 111 | data.transactionId = transactionId; 112 | saveTx(orderId, data); 113 | } 114 | res.send(response); 115 | }) 116 | .catch(function (err) { 117 | console.log('reservePaymentClient err', err); 118 | res.status(400).send(err); 119 | }); 120 | }; 121 | 122 | function confirmPaymentServer(req, res) { 123 | let { transactionId, orderId } = req.query; 124 | console.log('confirmPaymentServer', JSON.stringify(req.query)); 125 | let url = `${config.linepay.api}/v2/payments/${transactionId}/confirm`; 126 | let data; 127 | getOrderInfo(orderId) 128 | .then((orderInfo) => { 129 | data = orderInfo; 130 | let body = { 131 | amount: data.amount, 132 | currency: 'THB', 133 | }; 134 | let headers = { 135 | 'X-LINE-ChannelId': config.linepay.channelId, 136 | 'X-LINE-ChannelSecret': config.linepay.channelSecret, 137 | 'Content-Type': 'application/json', 138 | }; 139 | return rp({ 140 | method: 'POST', 141 | uri: url, 142 | body: body, 143 | headers, 144 | json: true, 145 | }); 146 | }) 147 | .then(function (response) { 148 | console.log('confirmPaymentServer response', JSON.stringify(response)); 149 | if (response && response.returnCode === '0000' && response.info) { 150 | data.status = 'paid'; 151 | saveTx(orderId, data); 152 | line.pushMessage(data.userId, lineHelper.createTextMessage('ได้รับชำระเงินเรียบร้อยแล้ว')); 153 | } 154 | res.send(response); 155 | }) 156 | .catch(function (err) { 157 | console.log('confirmPaymentServer err', err); 158 | res.status(400).send(err); 159 | }); 160 | }; 161 | 162 | function confirmPaymentClient(req, res) { 163 | let { transactionId, orderId } = req.query; 164 | let url = `${config.linepay.api}/v2/payments/${transactionId}/confirm`; 165 | let data; 166 | getOrderInfo(orderId) 167 | .then((orderInfo) => { 168 | data = orderInfo; 169 | let body = { 170 | amount: data.amount, 171 | currency: 'THB', 172 | }; 173 | let headers = { 174 | 'X-LINE-ChannelId': config.linepay.channelId, 175 | 'X-LINE-ChannelSecret': config.linepay.channelSecret, 176 | 'Content-Type': 'application/json', 177 | }; 178 | return rp({ 179 | method: 'POST', 180 | uri: url, 181 | body: body, 182 | headers, 183 | json: true, 184 | }); 185 | }) 186 | .then(function (response) { 187 | console.log('confirmPaymentClient response', JSON.stringify(response)); 188 | if (response && response.returnCode === '0000' && response.info) { 189 | data.status = 'paid'; 190 | saveTx(orderId, data); 191 | line.pushMessage(data.userId, lineHelper.createTextMessage('ได้รับชำระเงินเรียบร้อยแล้ว')); 192 | } 193 | res.send(response); 194 | }) 195 | .catch(function (err) { 196 | console.log('confirmPaymentClient err', err); 197 | res.status(400).send(err); 198 | }); 199 | }; 200 | 201 | function saveTx(orderId, object) { 202 | object['lastActionDate'] = Date.now(); 203 | var txRef = database.ref("/transactions/" + orderId); 204 | return txRef.update(object); 205 | } 206 | 207 | function getOrderInfo(orderId) { 208 | return new Promise((resolve, reject) => { 209 | let list = []; 210 | var txRef = database.ref("/transactions/" + orderId); 211 | txRef.once("value", function (snapshot) { 212 | list.push(snapshot.val()); 213 | if (list.length > 0) { 214 | resolve(list[0]); 215 | } else { 216 | reject(); 217 | } 218 | }); 219 | }); 220 | } --------------------------------------------------------------------------------