├── 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 |

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 | }
--------------------------------------------------------------------------------