Skip to content

Commit

Permalink
[PAY-1589] Wire up Stripe Onramp in mobile (#3814)
Browse files Browse the repository at this point in the history
  • Loading branch information
dharit-tan authored Jul 29, 2023
1 parent d09fc6e commit d2630a8
Show file tree
Hide file tree
Showing 13 changed files with 281 additions and 21 deletions.
4 changes: 4 additions & 0 deletions packages/common/src/store/account/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export const getAccountProfilePictureSizes = (state: CommonState) => {
export const getPlaylistLibrary = (state: CommonState) => {
return getAccountUser(state)?.playlist_library ?? null
}
export const getAccountERCWallet = createSelector(
[internalGetAccountUser],
(user) => user?.erc_wallet ?? null
)

/**
* Gets the account and full playlist metadatas.
Expand Down
2 changes: 2 additions & 0 deletions packages/mobile/.env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ RECAPTCHA_SITE_KEY=6LfVR-0ZAAAAADFcqNM1P1IafKwQwN0E_l-gxQ9q
HCAPTCHA_SITE_KEY=2abe61f1-af6e-4707-be19-a9a4146a9bea

OPENSEA_API_URL=https://rinkeby-api.opensea.io/api/v1

REACT_APP_STRIPE_CLIENT_PUBLISHABLE_KEY=
2 changes: 2 additions & 0 deletions packages/mobile/.env.prod
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,5 @@ FINGERPRINT_PUBLIC_API_KEY=MNtDQ4NCsNSP7YOkOiQT
FINGERPRINT_ENDPOINT=https://fp.audius.co

OLD_WEB_APP_STATIC_SERVER_PORT=3100

REACT_APP_STRIPE_CLIENT_PUBLISHABLE_KEY=pk_live_51LPsGuCJOWtpH6AEKshlCs3L8QhAfevNvhev8K9a0u92O5ku83KRjLIqCdxgf3NhitdtmMGlw0Wjf33NjZJjZUBz006A3IoSiQ
2 changes: 2 additions & 0 deletions packages/mobile/.env.stage
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,5 @@ FINGERPRINT_PUBLIC_API_KEY=Rz2A3Y5YGSg9K80VgKPi
FINGERPRINT_ENDPOINT=https://fp.staging.audius.co

OLD_WEB_APP_STATIC_SERVER_PORT=3101

REACT_APP_STRIPE_CLIENT_PUBLISHABLE_KEY=pk_test_51LPsGuCJOWtpH6AEZT3Wf2U2xmLZQrEV56yha7HEVTEyhYYVrWCdknml3t4gkSe9Nagd1o9Royy8zL3XEAmRzeHS00xAKTfgpi
85 changes: 84 additions & 1 deletion packages/mobile/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
"@snapchat/snap-kit-react-native": "0.4.0",
"@solana-mobile/mobile-wallet-adapter-protocol": "0.9.9",
"@solana-mobile/mobile-wallet-adapter-protocol-web3js": "0.9.9",
"@stripe/crypto": "0.0.4",
"@stripe/stripe-js": "1.54.1",
"@svgr/core": "5.5.0",
"@walletconnect/react-native-dapp": "1.8.0",
"array.prototype.flat": "1.2.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { useCallback } from 'react'

import {
cacheTracksSelectors,
formatUSDCWeiToUSDString,
Expand All @@ -9,14 +7,16 @@ import { View } from 'react-native'
import { useSelector } from 'react-redux'

import IconCart from 'app/assets/images/iconCart.svg'
import { LockedStatusBadge, Text, Button } from 'app/components/core'
import { LockedStatusBadge, Text } from 'app/components/core'
import { NativeDrawer } from 'app/components/drawer'
import { useDrawer } from 'app/hooks/useDrawer'
import { makeStyles, flexRowCentered } from 'app/styles'
import { useThemeColors } from 'app/utils/theme'
import { useColor } from 'app/utils/theme'

import { TrackDetailsTile } from '../track-details-tile'

import { StripePurchaseConfirmationButton } from './StripePurchaseConfirmationButton'

const { getTrack } = cacheTracksSelectors

const PREMIUM_TRACK_PURCHASE_MODAL_NAME = 'PremiumTrackPurchase'
Expand All @@ -31,8 +31,7 @@ const messages = {
price: (price: string) => `$${price}`,
payToUnlock: 'Pay-To-Unlock',
disclaimer:
'By clicking on "Buy", you agree to our Terms of Use. Your purchase will be made in USDC via 3rd party payment provider. Additional payment provider fees may apply. Any remaining USDC balance in your Audius wallet will be applied to this transaction. Once your payment is confirmed, your premium content will be unlocked and available to stream.',
buy: (price: string) => `Buy $${price}`
'By clicking on "Buy", you agree to our Terms of Use. Your purchase will be made in USDC via 3rd party payment provider. Additional payment provider fees may apply. Any remaining USDC balance in your Audius wallet will be applied to this transaction. Once your payment is confirmed, your premium content will be unlocked and available to stream.'
}

const useStyles = makeStyles(({ spacing, typography, palette }) => ({
Expand Down Expand Up @@ -95,15 +94,11 @@ const useStyles = makeStyles(({ spacing, typography, palette }) => ({

export const PremiumTrackPurchaseDrawer = () => {
const styles = useStyles()
const { neutralLight2, specialLightGreen1 } = useThemeColors()
const neutralLight2 = useColor('neutralLight2')
const { data } = useDrawer('PremiumTrackPurchase')
const { trackId } = data
const track = useSelector((state) => getTrack(state, { id: trackId }))

const handleConfirmPress = useCallback(() => {
console.log('buy button pressed')
}, [])

const { premium_conditions: premiumConditions } = track ?? {}

if (!track || !isPremiumContentUSDCPurchaseGated(premiumConditions))
Expand Down Expand Up @@ -153,14 +148,7 @@ export const PremiumTrackPurchaseDrawer = () => {
</View>
<Text>{messages.disclaimer}</Text>
</View>
<Button
title={messages.buy(price)}
onPress={handleConfirmPress}
variant={'primary'}
size='large'
color={specialLightGreen1}
fullWidth
/>
<StripePurchaseConfirmationButton price={price} />
</View>
</NativeDrawer>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useCallback } from 'react'

import { accountSelectors } from '@audius/common'
import { useSelector } from 'react-redux'

import { Button } from 'app/components/core'
import { useNavigation } from 'app/hooks/useNavigation'
import { createStripeSession, getUSDCUserBank } from 'app/services/buyCrypto'
import { useThemeColors } from 'app/utils/theme'

const { getAccountERCWallet } = accountSelectors

const messages = {
buy: (price: string) => `Buy $${price}`
}

type StripePurchaseConfirmationButtonProps = {
price: string
}

export const StripePurchaseConfirmationButton = ({
price
}: StripePurchaseConfirmationButtonProps) => {
const navigation = useNavigation()
const { specialLightGreen1 } = useThemeColors()
const ethWallet = useSelector(getAccountERCWallet)

const handleBuyPress = useCallback(async () => {
try {
if (ethWallet === null) {
throw new Error('Stripe session creation failed: no eth wallet found')
}
const usdcUserBank = await getUSDCUserBank(ethWallet)
if (usdcUserBank === undefined) {
throw new Error(
'Stripe session creation failed: could not get USDC user bank'
)
}
const res = await createStripeSession({
amount: price,
destinationWallet: usdcUserBank.toString()
})
if (res === undefined || res.client_secret === undefined) {
throw new Error(
'Stripe session creation failed: could not get client secret'
)
}
navigation.navigate('StripeOnrampEmbed', {
clientSecret: res.client_secret
})
} catch (e) {
console.error(e)
}
}, [ethWallet, navigation, price])

return (
<Button
onPress={handleBuyPress}
title={messages.buy(price)}
variant={'primary'}
size='large'
color={specialLightGreen1}
fullWidth
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useCallback } from 'react'

import { StyleSheet, View } from 'react-native'
import { WebView } from 'react-native-webview'

import { useIsUSDCEnabled } from 'app/hooks/useIsUSDCEnabled'
import { useRoute } from 'app/hooks/useRoute'
import { env } from 'app/services/env'

const STRIPE_PUBLISHABLE_KEY = env.REACT_APP_STRIPE_CLIENT_PUBLISHABLE_KEY

const styles = StyleSheet.create({
root: {
height: '100%',
width: '100%'
}
})

export const StripeOnrampEmbed = () => {
const { params } = useRoute<'StripeOnrampEmbed'>()
const { clientSecret } = params
const isUSDCEnabled = useIsUSDCEnabled()

const handleSessionUpdate = useCallback((event) => {
if (event?.payload?.session?.status) {
console.log(`Stripe Session Update ${event.payload.session.status}`)
}
}, [])

const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Crypto Onramp</title>
<meta name="description" content="A demo of hosted onramp" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://js.stripe.com/v3/"></script>
<script type="text/javascript" src="https://crypto-js.stripe.com/crypto-onramp-outer.js"></script>
</head>
<body>
<div id="onramp-element" />
<script type="text/javascript">
const handleSessionUpdate = (event) => {
window.ReactNativeWebView.postMessage(event)
}
try {
const onramp = new window.StripeOnramp("${STRIPE_PUBLISHABLE_KEY}")
const session = onramp.createSession({clientSecret:"${clientSecret}"})
session.mount('#onramp-element')
session.addEventListener('onramp_session_updated', handleSessionUpdate)
} catch (e) {
window.ReactNativeWebView.postMessage(e)
}
</script>
</body>
</html>
`

if (!STRIPE_PUBLISHABLE_KEY) {
console.error('Stripe publishable key not found')
return null
}

if (!clientSecret) {
console.error('Stripe client secret not found')
return null
}

if (!isUSDCEnabled) return null

return (
<View style={styles.root}>
<WebView
source={{ html }}
scrollEnabled={false}
onError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent
console.error('Stripe WebView onError: ', nativeEvent)
}}
onMessage={handleSessionUpdate}
/>
</View>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './StripeOnrampEmbed'
3 changes: 3 additions & 0 deletions packages/mobile/src/screens/app-screen/AppTabScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { FeatureFlags } from '@audius/common'
import type { EventArg, NavigationState } from '@react-navigation/native'
import type { createNativeStackNavigator } from '@react-navigation/native-stack'

import { StripeOnrampEmbed } from 'app/components/stripe-onramp-embed'
import { useDrawer } from 'app/hooks/useDrawer'
import { useFeatureFlag } from 'app/hooks/useRemoteConfig'
import { ChatListScreen } from 'app/screens/chat-screen/ChatListScreen'
Expand Down Expand Up @@ -111,6 +112,7 @@ export type AppTabScreenParamList = {
Chat: {
chatId: string
}
StripeOnrampEmbed: { clientSecret: string }
}

const forFade = ({ current }) => ({
Expand Down Expand Up @@ -335,6 +337,7 @@ export const AppTabScreen = ({ baseScreen, Stack }: AppTabScreenProps) => {
options={{ fullScreenGestureEnabled: false }}
/>
</Stack.Group>
<Stack.Screen name='StripeOnrampEmbed' component={StripeOnrampEmbed} />
</Stack.Navigator>
)
}
Loading

0 comments on commit d2630a8

Please sign in to comment.