From 8590d589c271b2eb19eb0b3a043d36f2c5e7a6e2 Mon Sep 17 00:00:00 2001 From: Leo Joseph <58416454+rleojoseph@users.noreply.github.com> Date: Thu, 16 Feb 2023 10:04:07 +0900 Subject: [PATCH] [MINI-1736] JS SDK API for In-App Purchase (#243) --- js-miniapp-bridge/src/common-bridge.ts | 76 +++- js-miniapp-bridge/src/index.ts | 8 + .../src/types/in-app-purchase.ts | 17 + .../src/types/response-types/miniapp/index.ts | 7 + js-miniapp-bridge/test/test.spec.ts | 60 +++ js-miniapp-sample/src/pages/auth-token.js | 8 +- js-miniapp-sample/src/pages/home.js | 4 +- .../src/pages/in-app-purchase.js | 369 ++++++++++++++++++ js-miniapp-sample/src/pages/landing.js | 4 + js-miniapp-sample/src/pages/local-storage.js | 5 +- js-miniapp-sample/src/pages/media.js | 1 + js-miniapp-sample/src/pages/share.js | 5 +- js-miniapp-sample/src/pages/uuid-sdk.js | 1 + js-miniapp-sample/src/pages/web-location.js | 8 +- js-miniapp-sample/src/routes.js | 12 + .../src/services/purchase/action.js | 83 ++++ .../src/services/purchase/reducers.js | 17 + .../src/services/purchase/types.js | 4 + js-miniapp-sample/src/services/reducers.js | 2 + js-miniapp-sdk/CHANGELOG.md | 5 + js-miniapp-sdk/README.md | 46 +++ js-miniapp-sdk/src/index.ts | 6 + js-miniapp-sdk/src/miniapp.ts | 2 + js-miniapp-sdk/src/modules/in-app-purchase.ts | 45 +++ js-miniapp-sdk/test/miniapp.spec.ts | 26 ++ 25 files changed, 806 insertions(+), 15 deletions(-) create mode 100644 js-miniapp-bridge/src/types/in-app-purchase.ts create mode 100644 js-miniapp-bridge/src/types/response-types/miniapp/index.ts create mode 100644 js-miniapp-sample/src/pages/in-app-purchase.js create mode 100644 js-miniapp-sample/src/services/purchase/action.js create mode 100644 js-miniapp-sample/src/services/purchase/reducers.js create mode 100644 js-miniapp-sample/src/services/purchase/types.js create mode 100644 js-miniapp-sdk/src/modules/in-app-purchase.ts diff --git a/js-miniapp-bridge/src/common-bridge.ts b/js-miniapp-bridge/src/common-bridge.ts index 538bbd8ea..a16171b70 100644 --- a/js-miniapp-bridge/src/common-bridge.ts +++ b/js-miniapp-bridge/src/common-bridge.ts @@ -26,6 +26,8 @@ import { import { ShareInfoType } from './types/share-info'; import { AccessTokenData, NativeTokenData } from './types/token-data'; import { MiniAppError, parseMiniAppError } from './types/error-types'; +import { MiniAppResponseInfo } from './types/response-types/miniapp'; +import { Product, PurchasedProduct } from './types/in-app-purchase'; /** @internal */ const mabMessageQueue: Callback[] = []; @@ -80,15 +82,17 @@ export class MiniAppBridge { this.executor = executor; this.platform = executor.getPlatform(); - window.addEventListener( - MiniAppSecureStorageEvents.onReady, - () => (this.isSecureStorageReady = true) - ); - window.addEventListener( - MiniAppSecureStorageEvents.onLoadError, - (e: CustomEvent) => - (this.secureStorageLoadError = parseMiniAppError(e.detail.message)) - ); + if (window) { + window.addEventListener( + MiniAppSecureStorageEvents.onReady, + () => (this.isSecureStorageReady = true) + ); + window.addEventListener( + MiniAppSecureStorageEvents.onLoadError, + (e: CustomEvent) => + (this.secureStorageLoadError = parseMiniAppError(e.detail.message)) + ); + } } /** @@ -680,6 +684,60 @@ export class MiniAppBridge { ); }); } + + /** + * This will retrieve the list of products details available for In-App Purchases associated with Mini App in the Platform. + * @returns List of In-app purchase products + * @see {getAllProducts} + */ + getAllProducts() { + return new Promise((resolve, reject) => { + return this.executor.exec( + 'getAllProducts', + null, + productsList => { + resolve(JSON.parse(productsList) as Product[]); + }, + error => reject(parseMiniAppError(error)) + ); + }); + } + + /** + * This will request for the In-App Purchase of a product with product id associated with Mini App in the Platform. + * @param id Product id of the product to be purchased. + * @returns Purchased product details and the transaction details of the purchase. + */ + purchaseProductWith(id: string) { + return new Promise((resolve, reject) => { + return this.executor.exec( + 'purchaseProductWith', + { product_id: id }, + purchasedProduct => { + resolve(JSON.parse(purchasedProduct) as PurchasedProduct); + }, + error => reject(parseMiniAppError(error)) + ); + }); + } + + /** + * This will request to Consume the product that is purchased using purchaseProductWith API + * @param id Product id of the product that is purchased. + * @returns + */ + consumePurchaseWith(id: string, transactionId: string) { + return new Promise((resolve, reject) => { + return this.executor.exec( + 'purchaseProductWith', + { product_id: id, transaction_id: transactionId }, + consumedInfo => { + resolve(JSON.parse(consumedInfo) as MiniAppResponseInfo); + }, + error => reject(parseMiniAppError(error)) + ); + }); + } } /** diff --git a/js-miniapp-bridge/src/index.ts b/js-miniapp-bridge/src/index.ts index e7e4edb6d..f027df142 100644 --- a/js-miniapp-bridge/src/index.ts +++ b/js-miniapp-bridge/src/index.ts @@ -44,6 +44,11 @@ import { MiniAppSecureStorageSize, MiniAppSecureStorageEvents, } from './types/secure-storage'; +import { + Product, + PurchasedProduct, + ProductPrice, +} from './types/in-app-purchase'; export { MiniAppBridge, @@ -80,4 +85,7 @@ export { SecureStorageIOError, MiniAppSecureStorageEvents, CloseAlertInfo, + Product, + PurchasedProduct, + ProductPrice, }; diff --git a/js-miniapp-bridge/src/types/in-app-purchase.ts b/js-miniapp-bridge/src/types/in-app-purchase.ts new file mode 100644 index 000000000..4001e1a43 --- /dev/null +++ b/js-miniapp-bridge/src/types/in-app-purchase.ts @@ -0,0 +1,17 @@ +export interface Product { + title: string; + description: string; + id: string; + price: ProductPrice; +} + +export interface ProductPrice { + currencyCode: string; + price: string; +} + +export interface PurchasedProduct { + product: Product; + transactionId: string; + transactionDate: string; +} diff --git a/js-miniapp-bridge/src/types/response-types/miniapp/index.ts b/js-miniapp-bridge/src/types/response-types/miniapp/index.ts new file mode 100644 index 000000000..5c3078afe --- /dev/null +++ b/js-miniapp-bridge/src/types/response-types/miniapp/index.ts @@ -0,0 +1,7 @@ +/** + * Generic Mini app Response object + */ +export interface MiniAppResponseInfo { + title?: string; + description?: string; +} diff --git a/js-miniapp-bridge/test/test.spec.ts b/js-miniapp-bridge/test/test.spec.ts index 77311f3ec..21b7917be 100644 --- a/js-miniapp-bridge/test/test.spec.ts +++ b/js-miniapp-bridge/test/test.spec.ts @@ -1007,6 +1007,66 @@ describe('console.log', () => { }); }); +describe('getAllProducts', () => { + it('will list the products', () => { + const bridge = new Bridge.MiniAppBridge(mockExecutor); + const response = + '[{"title": "MyApp_A","description": "This is app A for purchase","id": "com.rakuten.myappa","price": {"currencyCode": "yen","price": "100"}},{"title": "MyApp_B","description": "This is app B for purchase","id": "com.rakuten.myappb","price":{"currencyCode":"yen","price":"100"}}]'; + mockExecutor.exec.callsArgWith(2, response); + + const expected = [ + { + title: 'MyApp_A', + description: 'This is app A for purchase', + id: 'com.rakuten.myappa', + price: { + currencyCode: 'yen', + price: '100', + }, + }, + { + title: 'MyApp_B', + description: 'This is app B for purchase', + id: 'com.rakuten.myappb', + price: { + currencyCode: 'yen', + price: '100', + }, + }, + ]; + + return expect(bridge.getAllProducts()).to.eventually.deep.equal(expected); + }); +}); + +describe('purchaseProductWith', () => { + it('will purchase product with id', () => { + const bridge = new Bridge.MiniAppBridge(mockExecutor); + const response = + '[{"product": {"title": "MyApp_A","description": "This is app A for purchase","id": "com.rakuten.myappa","price": {"currencyCode": "yen","price": "100"}},"transactionId": "transction_id_a","transactionDate": "2023/02/14"}]'; + mockExecutor.exec.callsArgWith(2, response); + const expected = [ + { + product: { + title: 'MyApp_A', + description: 'This is app A for purchase', + id: 'com.rakuten.myappa', + price: { + currencyCode: 'yen', + price: '100', + }, + }, + transactionId: 'transction_id_a', + transactionDate: '2023/02/14', + }, + ]; + const productId = 'com.rakuten.myappa'; + return expect( + bridge.purchaseProductWith(productId) + ).to.eventually.deep.equal(expected); + }); +}); + interface CreateCallbackParams { onSuccess?: (success: any) => any; onError?: (error: string) => any; diff --git a/js-miniapp-sample/src/pages/auth-token.js b/js-miniapp-sample/src/pages/auth-token.js index 252722b23..6e5fd2cd3 100644 --- a/js-miniapp-sample/src/pages/auth-token.js +++ b/js-miniapp-sample/src/pages/auth-token.js @@ -28,6 +28,12 @@ import { requestCustomPermissions } from '../services/permissions/actions'; import { requestAccessToken } from '../services/user/actions'; const useStyles = makeStyles((theme) => ({ + card: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginTop: '40px', + }, wrapper: { position: 'relative', marginTop: 20, @@ -225,7 +231,7 @@ function AuthToken(props: AuthTokenProps) { } return ( - + diff --git a/js-miniapp-sample/src/pages/home.js b/js-miniapp-sample/src/pages/home.js index 097c3ac66..eaaed5ae0 100644 --- a/js-miniapp-sample/src/pages/home.js +++ b/js-miniapp-sample/src/pages/home.js @@ -9,8 +9,8 @@ import { import clsx from 'clsx'; import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; -import ToolBar from '../components/ToolBar'; import { navItems } from './../routes'; +import ToolBar from '../components/ToolBar'; const DRAWER_WIDTH = '250px'; const DRAWER_SHRINKED_WIDTH = '70px'; @@ -27,7 +27,7 @@ const useStyles = makeStyles((theme) => ({ height: '100%', display: 'flex', flexDirection: 'column', - justifyContent: 'center', + justifyContent: 'initial', alignItems: 'center', }, drawerClosed: { diff --git a/js-miniapp-sample/src/pages/in-app-purchase.js b/js-miniapp-sample/src/pages/in-app-purchase.js new file mode 100644 index 000000000..2902ac0e0 --- /dev/null +++ b/js-miniapp-sample/src/pages/in-app-purchase.js @@ -0,0 +1,369 @@ +import React, { useReducer } from 'react'; + +import { + Button, + CardHeader, + CircularProgress, + FormGroup, + Typography, + CardContent, + CardActions, + TextField, + Paper, +} from '@material-ui/core'; +import { red, green } from '@material-ui/core/colors'; +import { makeStyles } from '@material-ui/core/styles'; +import clsx from 'clsx'; +import { MiniAppError, PurchasedProductResponse } from 'js-miniapp-sdk'; +import { connect } from 'react-redux'; + +import GreyCard from '../components/GreyCard'; +import { + getAllProductsAction, + purchaseProductAction, + consumeProductAction, +} from '../services/purchase/action'; + +const useStyles = makeStyles((theme) => ({ + scrollable: { + overflowY: 'auto', + width: '100%', + paddingTop: 20, + paddingBottom: 20, + }, + card: { + width: '100%', + height: 'auto', + }, + root: { + background: theme.color.secondary, + width: '85vw', + maxWidth: 500, + }, + wrapper: { + position: 'relative', + marginTop: 10, + }, + buttonSuccess: { + backgroundColor: green[500], + '&:hover': { + backgroundColor: green[700], + }, + }, + buttonFailure: { + backgroundColor: red[500], + '&:hover': { + backgroundColor: red[700], + }, + }, + buttonProgress: { + position: 'absolute', + top: 'calc(50% - 10px)', + left: 'calc(50% - 10px)', + }, + error: { + color: red[500], + marginTop: 10, + }, + success: { + color: green[500], + marginTop: 20, + }, + rootUserGroup: { + alignItems: 'center', + }, + formInput: { + width: '90%', + marginTop: 10, + }, + rootCardActions: { + justifyContent: 'center', + }, + caseSelector: { + marginTop: 5, + }, + button: { + marginBottom: 15, + }, + dataFormsWrapper: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + paper: { + width: '100%', + paddingBottom: 10, + marginBottom: 20, + '&:last-child': { + marginBottom: 0, + }, + }, + red: { + color: red[500], + }, +})); + +export const initialState = { + isLoading: false, + isError: false, + error: null, +}; + +type State = { + isLoading: ?boolean, + isError: ?boolean, +}; + +type Action = { + type: string, + miniAppError: MiniAppError, +}; + +export const dataFetchReducer = (state: State, action: Action) => { + switch (action.type) { + case 'PURCHASE_FETCH_INIT': + return { + ...state, + isLoading: true, + isError: false, + error: null, + }; + case 'PURCHASE_FETCH_SUCCESS': + return { + ...state, + isLoading: false, + isError: false, + error: null, + }; + case 'PURCHASE_FETCH_FAILURE': + return { + ...initialState, + isLoading: false, + isError: true, + error: + (typeof action.miniAppError == 'string' + ? action.miniAppError + : action.miniAppError.message) || '', + }; + + default: + throw Error('Unknown action type'); + } +}; + +type PurchaseProductProps = { + purchasedProduct: PurchasedProductResponse, + purchaseProductWith: (itemId: string) => Promise, + consumeProductWith: (itemId: string) => Promise, + purchaseError: MiniAppError, +}; + +function PurchaseComponent(props: PurchaseProductProps) { + const [state, dispatch] = useReducer(dataFetchReducer, initialState); + const classes = useStyles(); + + let inputValue = ''; + + const handleInput = (e: SyntheticInputEvent) => { + e.preventDefault(); + inputValue = e.currentTarget.value; + }; + + const buttonClassname = clsx({ + [classes.buttonFailure]: state.isError, + [classes.buttonSuccess]: !state.isError, + }); + + function handlePurchaseClick(e) { + if (!state.isLoading) { + dispatch({ type: 'PURCHASE_FETCH_INIT', miniAppError: null }); + BuyProduct(); + } + } + + function BuyProduct() { + props + .purchaseProductWith(inputValue) + .then(() => + dispatch({ type: 'PURCHASE_FETCH_SUCCESS', miniAppError: null }) + ) + .catch((miniAppError) => { + console.log('Product Error: ', miniAppError); + dispatch({ type: 'PURCHASE_FETCH_FAILURE', miniAppError }); + }); + } + + function handleConsumeClick(e) { + if (!state.isLoading) { + dispatch({ type: 'PURCHASE_FETCH_INIT', miniAppError: null }); + ConsumeProduct(); + } + } + + function ConsumeProduct() { + props + .purchaseProductWith(inputValue) + .then(() => + dispatch({ type: 'PURCHASE_FETCH_SUCCESS', miniAppError: null }) + ) + .catch((miniAppError) => { + console.log('Product Error: ', miniAppError); + dispatch({ type: 'PURCHASE_FETCH_FAILURE', miniAppError }); + }); + } + + function PurchaseProduct() { + return ( + + + + + ); + } + + function TransactionDetails() { + const dateInfo = new Date(props.purchasedProduct.product.transactionDate); + return ( + + + Transaction - {props.purchasedProduct.status} + + + Transaction Date: {dateInfo.toLocaleDateString()} +
+ Transaction Time: {dateInfo.toLocaleTimeString()} +
+ Transaction ID: {props.purchasedProduct.product.transactionId} +
+
+ ); + } + + function ShowPurchasedProductDetails() { + return ( + + + {!state.isLoading && !state.isError && props.purchasedProduct && ( + + {TransactionDetails()} +
+ Product Info + + ID: {props.purchasedProduct.product.productInfo.id}
+ Title: {props.purchasedProduct.product.productInfo.title}
+ Description:{' '} + {props.purchasedProduct.product.productInfo.description}
+
+
+
+ )} +
+ ); + } + + function PurchaseProductCardActionsForm() { + return ( + +
+
+ + + {state.isLoading && ( + + )} +
+
+ + + {state.isLoading && ( + + )} +
+
+ {!state.isLoading && state.isError && ( + + {state.error} + + )} +
+ ); + } + + return ( +
+ + +
+ {PurchaseProduct()} + {ShowPurchasedProductDetails()} +
+
+ + {PurchaseProductCardActionsForm()} + +
+
+ ); +} + +const mapStateToProps = (state) => { + console.log('MapStateToProps: ', state); + return { + purchasedProduct: state.purchaseProduct, + purchaseError: state.error, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + getAllProducts: () => dispatch(getAllProductsAction()), + purchaseProductWith: (itemId: string) => + dispatch(purchaseProductAction(itemId)), + consumeProductWith: (itemId: string, transactionId: string) => + dispatch(consumeProductAction(itemId)), + }; +}; + +export { PurchaseComponent }; +export default connect(mapStateToProps, mapDispatchToProps)(PurchaseComponent); diff --git a/js-miniapp-sample/src/pages/landing.js b/js-miniapp-sample/src/pages/landing.js index bdcf8aa64..2b55f1c53 100644 --- a/js-miniapp-sample/src/pages/landing.js +++ b/js-miniapp-sample/src/pages/landing.js @@ -26,6 +26,7 @@ const useStyles = makeStyles((theme) => ({ display: 'flex', alignItems: 'center', justifyContent: 'center', + marginTop: '40px', }, content: { height: '25%', @@ -51,6 +52,9 @@ const useStyles = makeStyles((theme) => ({ minHeight: 40, margin: 0, }, + paddingTop50: { + marginTop: '50px', + }, })); const Landing = (props: LandingProps) => { diff --git a/js-miniapp-sample/src/pages/local-storage.js b/js-miniapp-sample/src/pages/local-storage.js index 2bfbe1c38..52e88a05c 100644 --- a/js-miniapp-sample/src/pages/local-storage.js +++ b/js-miniapp-sample/src/pages/local-storage.js @@ -22,6 +22,9 @@ const useStyles = makeStyles((theme) => ({ color: theme.color.primary, fontWeight: 'bold', }, + card: { + marginTop: '40px', + }, actions: { justifyContent: 'center', flexDirection: 'column', @@ -62,7 +65,7 @@ function LocalStorage() { }; return ( - + ({ card: { height: 'auto', + marginTop: '40px', }, content: { justifyContent: 'center', diff --git a/js-miniapp-sample/src/pages/share.js b/js-miniapp-sample/src/pages/share.js index 6df87b81e..4e01f846f 100644 --- a/js-miniapp-sample/src/pages/share.js +++ b/js-miniapp-sample/src/pages/share.js @@ -12,6 +12,9 @@ import MiniApp from 'js-miniapp-sdk'; import GreyCard from '../components/GreyCard'; const useStyles = makeStyles((theme) => ({ + card: { + marginTop: '40px', + }, content: { height: '50%', justifyContent: 'center', @@ -59,7 +62,7 @@ function Share() { }; return ( - + ({ card: { width: '100%', height: '250px', + marginTop: '40px', }, content: { height: '16%', diff --git a/js-miniapp-sample/src/pages/web-location.js b/js-miniapp-sample/src/pages/web-location.js index 3e0f80d65..a36d24682 100644 --- a/js-miniapp-sample/src/pages/web-location.js +++ b/js-miniapp-sample/src/pages/web-location.js @@ -15,6 +15,12 @@ import GreyCard from '../components/GreyCard'; import useGeoLocation from '../hooks/useGeoLocation'; const useStyles = makeStyles((theme) => ({ + card: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginTop: '40px', + }, content: { height: '50%', justifyContent: 'center', @@ -63,7 +69,7 @@ const Location = (props: any) => { const [state, watch, unwatch] = useGeoLocation(); return ( - + {state.error &&
Error: {state.error}
} diff --git a/js-miniapp-sample/src/routes.js b/js-miniapp-sample/src/routes.js index aadfcf389..0d5b42b9c 100644 --- a/js-miniapp-sample/src/routes.js +++ b/js-miniapp-sample/src/routes.js @@ -18,6 +18,7 @@ import PhotoCamera from '@material-ui/icons/PhotoCamera'; import SecurityIcon from '@material-ui/icons/Security'; import SendIcon from '@material-ui/icons/SendSharp'; import ShareIcon from '@material-ui/icons/Share'; +import ShoppingCartIcon from '@material-ui/icons/ShoppingCart'; import StorageIcon from '@material-ui/icons/Storage'; import VpnKeyIcon from '@material-ui/icons/VpnKey'; @@ -29,6 +30,7 @@ import EventListener from './pages/event-listener'; import FileDownload from './pages/file-download'; import FileUploader from './pages/file-upload'; import GifPage from './pages/gifs'; +import { PurchaseComponent } from './pages/in-app-purchase'; import Landing from './pages/landing'; import LocalStorage from './pages/local-storage'; import Media from './pages/media'; @@ -99,6 +101,10 @@ const universalBridgeNavLink = { navLink: '/universal-bridge', label: 'Universal Bridge', }; +const inAppPurchaseNavLink = { + navLink: '/in-app-purchase', + label: 'Purchase', +}; const navLinks = [ iosHomeNavLink, @@ -250,6 +256,12 @@ const appItems = [ navLink: universalBridgeNavLink.navLink, element: , }, + { + icon: , + label: inAppPurchaseNavLink.label, + navLink: inAppPurchaseNavLink.navLink, + element: , + }, ]; const navItems: Object[] = homeItem.concat( diff --git a/js-miniapp-sample/src/services/purchase/action.js b/js-miniapp-sample/src/services/purchase/action.js new file mode 100644 index 000000000..59bfe4cf4 --- /dev/null +++ b/js-miniapp-sample/src/services/purchase/action.js @@ -0,0 +1,83 @@ +import MiniApp, { PurchasedProductResponse } from 'js-miniapp-sdk'; + +import { + REQUEST_PRODUCT_PURCHASE_SUCCESS, + REQUEST_PRODUCT_PURCHASE_FAILURE, +} from './types'; + +type PurchaseProductSuccessAction = { + type: string, + purchasedProduct: PurchasedProductResponse, + productsList: Product[], +}; + +const getAllProductsAction = (): Function => { + return (dispatch) => { + return MiniApp.purchaseService + .getAllProducts() + .then((products) => { + console.log('getAllProducts Success Action: ', products); + dispatch({ + type: REQUEST_PRODUCT_PURCHASE_SUCCESS, + productsList: products, + }); + return Promise.resolve(products); + }) + .catch((e) => { + console.log('getAllProducts Error: ', e); + dispatch({ + type: REQUEST_PRODUCT_PURCHASE_FAILURE, + }); + throw e; + }); + }; +}; + +const purchaseProductAction = (itemId: string): Function => { + return (dispatch) => { + return MiniApp.purchaseService + .purchaseItemWith(itemId) + .then((purchasedProduct) => { + console.log('PurchaseProductSuccessAction: ', purchasedProduct); + dispatch({ + type: REQUEST_PRODUCT_PURCHASE_SUCCESS, + purchasedProduct: purchasedProduct, + }); + return Promise.resolve(purchasedProduct); + }) + .catch((e) => { + console.log('PurchaseProduct Error: ', e); + dispatch({ + type: REQUEST_PRODUCT_PURCHASE_FAILURE, + }); + throw e; + }); + }; +}; + +const consumeProductAction = ( + itemId: string, + transactionId: string +): Function => { + return (dispatch) => { + return MiniApp.purchaseService + .consumePurchaseWith(itemId, transactionId) + .then((miniAppResponseInfo) => { + console.log('consumeProductAction: ', miniAppResponseInfo); + dispatch({ + type: REQUEST_PRODUCT_PURCHASE_SUCCESS, + }); + return Promise.resolve(miniAppResponseInfo); + }) + .catch((e) => { + console.log('consumeProductAction Error: ', e); + dispatch({ + type: REQUEST_PRODUCT_PURCHASE_FAILURE, + }); + throw e; + }); + }; +}; + +export { getAllProductsAction, purchaseProductAction, consumeProductAction }; +export type { PurchaseProductSuccessAction }; diff --git a/js-miniapp-sample/src/services/purchase/reducers.js b/js-miniapp-sample/src/services/purchase/reducers.js new file mode 100644 index 000000000..a62f7779f --- /dev/null +++ b/js-miniapp-sample/src/services/purchase/reducers.js @@ -0,0 +1,17 @@ +import type { PurchaseProductSuccessAction } from './actions'; +import { REQUEST_PRODUCT_PURCHASE_SUCCESS } from './types'; + +const defaultPurchaseProduct = null; +const PurchaseProductReducer = ( + state: ?string = defaultPurchaseProduct, + action: PurchaseProductSuccessAction +): ?string => { + switch (action.type) { + case REQUEST_PRODUCT_PURCHASE_SUCCESS: + return action.purchasedProduct; + default: + return state; + } +}; + +export { PurchaseProductReducer }; \ No newline at end of file diff --git a/js-miniapp-sample/src/services/purchase/types.js b/js-miniapp-sample/src/services/purchase/types.js new file mode 100644 index 000000000..9fad7b5cf --- /dev/null +++ b/js-miniapp-sample/src/services/purchase/types.js @@ -0,0 +1,4 @@ +const REQUEST_PRODUCT_PURCHASE_SUCCESS = 'REQUEST_PRODUCT_PURCHASE_SUCCESS'; +const REQUEST_PRODUCT_PURCHASE_FAILURE = 'REQUEST_PRODUCT_PURCHASE_FAILURE'; + +export { REQUEST_PRODUCT_PURCHASE_SUCCESS, REQUEST_PRODUCT_PURCHASE_FAILURE }; \ No newline at end of file diff --git a/js-miniapp-sample/src/services/reducers.js b/js-miniapp-sample/src/services/reducers.js index 78b6516e9..3e3c53861 100644 --- a/js-miniapp-sample/src/services/reducers.js +++ b/js-miniapp-sample/src/services/reducers.js @@ -8,6 +8,7 @@ import { } from './landing/reducers'; import MessageReducer from './message/reducers'; import { PermissionsReducer } from './permissions/reducers'; +import { PurchaseProductReducer } from './purchase/reducers'; import storageReducer from './secure-storage/reducers'; import userReducer from './user/reducers'; import { UUIDReducer } from './uuid/reducers'; @@ -22,4 +23,5 @@ export default combineReducers({ file: FileDownloadReducer, secureStorage: storageReducer, secureStorageStatus: SecureStorageStatusReducer, + purchaseProduct: PurchaseProductReducer, }); diff --git a/js-miniapp-sdk/CHANGELOG.md b/js-miniapp-sdk/CHANGELOG.md index c76628577..f13bbc8c1 100644 --- a/js-miniapp-sdk/CHANGELOG.md +++ b/js-miniapp-sdk/CHANGELOG.md @@ -1,5 +1,10 @@ ## CHANGELOG +### 1.17.0 (2023-xx-xx) +- **Feature:** Added In-app purchases related interfaces. `getAllProducts()` , `purchaseProductWith(id: string)`, `consumePurchaseWith(id: string, transactionId: string)`. These interfaces can be used to prepare the list of products and make a in-app purchase with product id from associated with Mini App platform. + +--- + ### 1.16.0 (2023-01-30) - **Feature:** Added Universal Bridge related interface e.g. `sendJsonToHostapp` to send any JSON/String to the HostApp. Also, added support with `HostAppEvents` to receive any JSON/String to MiniApp from the HostApp. - **Feature:** Added `allEmailList` property in `Contact` to support multiple email address of a specific contact. diff --git a/js-miniapp-sdk/README.md b/js-miniapp-sdk/README.md index 03e79e570..9bc8c0431 100644 --- a/js-miniapp-sdk/README.md +++ b/js-miniapp-sdk/README.md @@ -702,6 +702,52 @@ MiniApp.miniappUtils.closeMiniApp(true).catch((error) => { +## In-App Purchases +You can perform the in-app purchases for the products available for In-App Purchases associated with Google Play™. +### Get all products list +This will retrieve the list of products details available for In-App Purchase associated with Google Play™. +This will return only the list of products associated with Mini app in the platform +```javascript +import miniApp from 'js-miniapp-sdk'; +miniApp.purchases + .getAllProducts() + .then((success) => { + console.log(success); + }) + .catch((error) => { + console.error(error); + }); +``` +#### Purchase a product with product id +This will request for the In-app Purchase of a product with product id associated with Google Play™. +Returns the PurchasedProduct object with transaction details. +```javascript +import miniApp from 'js-miniapp-sdk'; +miniApp.purchases + .purchaseProductWith(productId) + .then((success) => { + console.log(success); + }) + .catch((error) => { + console.error(error); + }); +``` + +#### Consume a purchase +This will request to Consume the product that is purchased using the purchaseProductWith API +Returns the PurchasedProduct object with transaction details. +```javascript +import miniApp from 'js-miniapp-sdk'; +miniApp.purchases + .consumePurchaseWith(productId, transactionId) + .then((success) => { + console.log(success); + }) + .catch((error) => { + console.error(error); + }); +``` + ## Advanced Usage
diff --git a/js-miniapp-sdk/src/index.ts b/js-miniapp-sdk/src/index.ts index 13bca67fa..13b2e4338 100644 --- a/js-miniapp-sdk/src/index.ts +++ b/js-miniapp-sdk/src/index.ts @@ -33,6 +33,9 @@ import { SecureStorageIOError, MiniAppSecureStorageEvents, CloseAlertInfo, + Product, + ProductPrice, + PurchasedProduct, } from '../../js-miniapp-bridge/src'; import { MiniApp } from './miniapp'; @@ -78,4 +81,7 @@ export { SecureStorageIOError, MiniAppSecureStorageEvents, CloseAlertInfo, + Product, + ProductPrice, + PurchasedProduct, }; diff --git a/js-miniapp-sdk/src/miniapp.ts b/js-miniapp-sdk/src/miniapp.ts index 852b4f011..784c80dca 100644 --- a/js-miniapp-sdk/src/miniapp.ts +++ b/js-miniapp-sdk/src/miniapp.ts @@ -20,6 +20,7 @@ import { deprecate } from 'util'; import { SecureStorageService } from './modules/secure-storage'; import { UniversalBridge } from './modules/universal-bridge'; import { MiniAppUtils } from './modules/utils'; +import { Purchases } from './modules/in-app-purchase'; /** * A module layer for webapps and mobile native interaction. @@ -165,6 +166,7 @@ export class MiniApp implements MiniAppFeatures, Ad, Platform { secureStorageService = new SecureStorageService(); universalBridge = new UniversalBridge(); miniappUtils = new MiniAppUtils(); + purchaseService = new Purchases(); private requestPermission(permissionType: DevicePermission): Promise { return getBridge().requestPermission(permissionType); diff --git a/js-miniapp-sdk/src/modules/in-app-purchase.ts b/js-miniapp-sdk/src/modules/in-app-purchase.ts new file mode 100644 index 000000000..582770e3e --- /dev/null +++ b/js-miniapp-sdk/src/modules/in-app-purchase.ts @@ -0,0 +1,45 @@ +import { Product, PurchasedProduct } from '../../../js-miniapp-bridge/src'; +import { MiniAppResponseInfo } from '../../../js-miniapp-bridge/src/types/response-types/miniapp'; +import { getBridge } from '../sdkbridge'; + +interface PurchaseProvider { + /** + * Retrieves and lists all the products from the play/app store which are available for inapp-purchases. + */ + getAllProducts(): Promise; + + /** + * Triggers the request to host app to Purchase a product using the Product ID. + * @param id The product id which must be purchased from inapp-purchase. + * This will return the status of inapp-purchase and the details of the purchased product. + */ + purchaseProductWith(id: string): Promise; + + /** + * Triggers the request to host app to Purchase a product using the Product ID. + * @param id The product id which must be purchased from inapp-purchase. + * This will return the status of inapp-purchase and the details of the purchased product. + */ + consumePurchaseWith( + id: string, + transactionId: string + ): Promise; +} + +/** @internal */ +export class Purchases { + getAllProducts(): Promise { + return getBridge().getAllProducts(); + } + + purchaseProductWith(id: string): Promise { + return getBridge().purchaseProductWith(id); + } + + consumePurchaseWith( + id: string, + transactionId: string + ): Promise { + return getBridge().consumePurchaseWith(id, transactionId); + } +} diff --git a/js-miniapp-sdk/test/miniapp.spec.ts b/js-miniapp-sdk/test/miniapp.spec.ts index 216f66ef8..30b7db1aa 100644 --- a/js-miniapp-sdk/test/miniapp.spec.ts +++ b/js-miniapp-sdk/test/miniapp.spec.ts @@ -63,6 +63,8 @@ window.MiniAppBridge = { isSecureStorageReady: sandbox.stub(), sendJsonToHostapp: sandbox.stub(), closeMiniApp: sandbox.stub(), + getAllProducts: sandbox.stub(), + purchaseProductWith: sandbox.stub(), }; const miniApp = new MiniApp(); const messageToContact: MessageToContact = { @@ -839,3 +841,27 @@ describe('closeMiniApp', () => { ); }); }); + +describe('purchaseProductWith', () => { + it('should Purchases the app with given id', () => { + const response = { + product: { + title: 'MyApp_A', + description: 'This is app A for purchase', + id: 'com.rakuten.myappa', + price: { + currencyCode: 'yen', + price: '100', + }, + }, + transactionId: 'transction_id_a', + transactionDate: '2022/11/23', + }; + const productId = 'com.rakuten.myappa'; + + window.MiniAppBridge.purchaseProductWith.resolves(response); + return expect( + miniApp.purchaseService.purchaseProductWith(productId) + ).to.eventually.equal(response); + }); +});