Skip to content

Commit

Permalink
feat(Paywall): Add IAP support
Browse files Browse the repository at this point in the history
BREAKING CHANGE:
To check if IAP is available, you need to wrap your Paywall into a `WebviewContext` component
```
import { WebviewIntentProvider } from 'cozy-intent'

<WebviewIntentProvider>
  <Paywall />
</WebviewIntentProvider>
```
  • Loading branch information
cballevre committed Dec 15, 2023
1 parent 01ec4de commit 7b54299
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 30 deletions.
28 changes: 24 additions & 4 deletions react/Paywall/Paywall.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import PropTypes from 'prop-types'

import { isFlagshipApp } from 'cozy-device-helper'
import { useInstanceInfo } from 'cozy-client'
import { buildPremiumLink } from 'cozy-client/dist/models/instance'
import flag from 'cozy-flags'
import { useWebviewIntent } from 'cozy-intent'

import Spinner from '../Spinner'
import { IllustrationDialog } from '../CozyDialogs'
Expand All @@ -24,6 +25,19 @@ const Paywall = ({ variant, onClose, isPublic, contentInterpolation }) => {
const instanceInfo = useInstanceInfo()
const { t } = useI18n()

const webviewIntent = useWebviewIntent()
const [isFlagshipAppIapAvailable, setFlagshipAppIapAvailable] = useState(null)

useEffect(() => {
const fetchIapAvailability = async () => {
const isAvailable =
(await webviewIntent?.call('isAvailable', 'iap')) ?? false
setFlagshipAppIapAvailable(isAvailable)
}

fetchIapAvailability()
}, [webviewIntent])

if (!instanceInfo.isLoaded)
return (
<IllustrationDialog
Expand All @@ -39,7 +53,10 @@ const Paywall = ({ variant, onClose, isPublic, contentInterpolation }) => {
)

const canOpenPremiumLink =
!isFlagshipApp() || (isFlagshipApp() && !!flag('flagship.iap.enabled'))
!isFlagshipApp() ||
(isFlagshipApp() &&
!!flag('flagship.iap.enabled') &&
isFlagshipAppIapAvailable)

const link = buildPremiumLink(instanceInfo)
const type = makeType(instanceInfo, isPublic, link)
Expand Down Expand Up @@ -67,10 +84,13 @@ const Paywall = ({ variant, onClose, isPublic, contentInterpolation }) => {
<Button
onClick={onAction}
label={
canOpenPremiumLink
isFlagshipAppIapAvailable === null
? t(`action.loading`)
: canOpenPremiumLink
? t(`${variant}Paywall.${type}.action`)
: t(`mobileApp.action`)
: t(`action.withoutIAP`)
}
busy={isFlagshipAppIapAvailable === null}
/>
}
content={
Expand Down
66 changes: 48 additions & 18 deletions react/Paywall/Paywall.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { render, screen, fireEvent } from '@testing-library/react'
import { createMockClient, useInstanceInfo } from 'cozy-client'
import { isFlagshipApp } from 'cozy-device-helper'
import flag from 'cozy-flags'
import { useWebviewIntent } from 'cozy-intent'

import DemoProvider from '../providers/DemoProvider'
import Paywall from './Paywall'
Expand All @@ -16,6 +17,10 @@ jest.mock('cozy-client', () => ({
...jest.requireActual('cozy-client'),
useInstanceInfo: jest.fn()
}))
jest.mock('cozy-intent', () => ({
...jest.requireActual('cozy-intent'),
useWebviewIntent: jest.fn()
}))
jest.mock('cozy-flags')

describe('Paywall', () => {
Expand All @@ -31,7 +36,8 @@ describe('Paywall', () => {
enablePremiumLinks = false,
hasUuid = false,
isFlagshipApp: isFlagshipAppReturnValue = false,
isIapEnabled = null
isIapEnabled = null,
isIapAvailable = false
} = {}) => {
useInstanceInfo.mockReturnValue({
context: {
Expand All @@ -52,6 +58,10 @@ describe('Paywall', () => {

isFlagshipApp.mockReturnValue(isFlagshipAppReturnValue)
flag.mockReturnValue(isIapEnabled)
const mockCall = jest.fn().mockResolvedValue(isIapAvailable)
useWebviewIntent.mockReturnValue({
call: mockCall
})

const mockClient = createMockClient({})
return render(
Expand All @@ -65,41 +75,41 @@ describe('Paywall', () => {
)
}

it('should display the default case when nothing is defined', () => {
it('should display the default case when nothing is defined', async () => {
setup()

expect(screen.getByText('Information')).toBeInTheDocument()

const actionButton = screen.getByRole('button', {
const actionButton = await screen.findByRole('button', {
name: 'I understand'
})
fireEvent.click(actionButton)
expect(onCloseSpy).toBeCalledTimes(1)
})

it('should display the default case when there is a premium link but it is not enabled', () => {
it('should display the default case when there is a premium link but it is not enabled', async () => {
setup({
hasUuid: true
})

expect(screen.getByText('Information')).toBeInTheDocument()

const actionButton = screen.getByRole('button', {
const actionButton = await screen.findByRole('button', {
name: 'I understand'
})
fireEvent.click(actionButton)
expect(onCloseSpy).toBeCalledTimes(1)
})

it('should display the premium case when the premium link is enabled and available', () => {
it('should display the premium case when the premium link is enabled and available', async () => {
setup({
hasUuid: true,
enablePremiumLinks: true
})

expect(screen.getByText('Upgrade your plan')).toBeInTheDocument()

const actionButton = screen.getByRole('button', {
const actionButton = await screen.findByRole('button', {
name: 'Check our plans'
})
fireEvent.click(actionButton)
Expand All @@ -109,7 +119,7 @@ describe('Paywall', () => {
)
})

it('should display the public case when the premium link is available in public context (eg. sharing view)', () => {
it('should display the public case when the premium link is available in public context (eg. sharing view)', async () => {
setup({
hasUuid: true,
enablePremiumLinks: true,
Expand All @@ -122,14 +132,14 @@ describe('Paywall', () => {
)
).toBeInTheDocument()

const actionButton = screen.getByRole('button', {
const actionButton = await screen.findByRole('button', {
name: 'I understand'
})
fireEvent.click(actionButton)
expect(onCloseSpy).toBeCalledTimes(1)
})

it('should display the default case when the premium link is not available in public context', () => {
it('should display the default case when the premium link is not available in public context', async () => {
setup({
hasUuid: true,
enablePremiumLinks: false,
Expand All @@ -138,15 +148,15 @@ describe('Paywall', () => {

expect(screen.getByText('Information')).toBeInTheDocument()

const actionButton = screen.getByRole('button', {
const actionButton = await screen.findByRole('button', {
name: 'I understand'
})
fireEvent.click(actionButton)
expect(onCloseSpy).toBeCalledTimes(1)
})

describe('on flagship', () => {
it('should display the premium case without an action button to access the premium link', () => {
it('should display the premium case without an action button to access the premium link', async () => {
setup({
hasUuid: true,
enablePremiumLinks: true,
Expand All @@ -155,14 +165,14 @@ describe('Paywall', () => {

expect(screen.getByText('Upgrade your plan')).toBeInTheDocument()

const actionButton = screen.getByRole('button', {
const actionButton = await screen.findByRole('button', {
name: 'I understand'
})
fireEvent.click(actionButton)
expect(onCloseSpy).toBeCalledTimes(1)
})

it('should display the premium case without an action button to access the premium link when flag flagship.iap.enabled is false', () => {
it('should display the premium case without an action button to access the premium link when flag flagship.iap.enabled is false', async () => {
setup({
hasUuid: true,
enablePremiumLinks: true,
Expand All @@ -172,24 +182,44 @@ describe('Paywall', () => {

expect(screen.getByText('Upgrade your plan')).toBeInTheDocument()

const actionButton = screen.getByRole('button', {
const actionButton = await screen.findByRole('button', {
name: 'I understand'
})
fireEvent.click(actionButton)
expect(onCloseSpy).toBeCalledTimes(1)
})

it('should display the premium case with an action button to access the premium link when flag flagship.iap.enabled is true', () => {
it('should display the premium case without an action button to access the premium link when flag flagship.iap.enabled is false and iap is unavailable', async () => {
setup({
hasUuid: true,
enablePremiumLinks: true,
isFlagshipApp: true,
isIapEnabled: false,
isIapAvailable: false
})

expect(screen.getByText('Upgrade your plan')).toBeInTheDocument()

const actionButton = await screen.findByRole('button', {
name: 'I understand'
})

fireEvent.click(actionButton)
expect(onCloseSpy).toBeCalledTimes(1)
})

it('should display the premium case with an action button to access the premium link when flag flagship.iap.enabled is true and iap is available', async () => {
setup({
hasUuid: true,
enablePremiumLinks: true,
isFlagshipApp: true,
isIapEnabled: true
isIapEnabled: true,
isIapAvailable: true
})

expect(screen.getByText('Upgrade your plan')).toBeInTheDocument()

const actionButton = screen.getByRole('button', {
const actionButton = await screen.findByRole('button', {
name: 'Check our plans'
})
fireEvent.click(actionButton)
Expand Down
15 changes: 12 additions & 3 deletions react/Paywall/Readme.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
A paywall is a modal designed to restrict access to a feature to encourage upgrading.
There is different variant for each features so the wording can be different to adapt to the context of use.

When we're in our mobile app called Flagship, we can't display the premium action instead, we just display "I understand" which closes the paywall. This is because our subscription process does not comply with the app store policies. When In app payement (iap) will be implemented we can display premium action back with flag `flagship.iap.enabled`
When we are in our mobile application called Flagship, we only display the premium action when IAP (In-App Payment) is activated.
Otherwise our web subscription process does not comply with the app store policies.
To check if IAP is enabled, the following points are verified:

1. The functionality is available with `cozy-intent`.
2. The flag `flagship.iap.enabled` is set to true.

### Usage

To use the Paywall component, it should be wrapped into a `WebviewContext` component.

### Variants

Expand All @@ -17,7 +26,7 @@ import {
import Variants from 'cozy-ui/docs/components/Variants'
import DemoProvider from 'cozy-ui/docs/components/DemoProvider'
import Button from 'cozy-ui/transpiled/react/Buttons'
import { createDemoClient } from 'cozy-client'
import { createFakeClient } from 'cozy-client'

const initialVariants = [
{
Expand Down Expand Up @@ -63,7 +72,7 @@ const togglePaywall = paywall => {
}

const makeClient = premiumLink =>
createDemoClient({
createFakeClient({
queries: {
'io.cozy.settings/io.cozy.settings.instance': {
doctype: 'io.cozy.settings',
Expand Down
5 changes: 3 additions & 2 deletions react/Paywall/locales/en.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"mobileApp": {
"action": "I understand"
"action": {
"loading": "Loading...",
"withoutIAP": "I understand"
},
"onlyOfficePaywall": {
"premium": {
Expand Down
5 changes: 3 additions & 2 deletions react/Paywall/locales/fr.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"mobileApp": {
"action": "J'ai compris"
"action": {
"loading": "Chargement...",
"withoutIAP": "J'ai compris"
},
"onlyOfficePaywall": {
"premium": {
Expand Down
2 changes: 1 addition & 1 deletion react/Paywall/locales/withPaywallLocales.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import withOnlyLocales from '../../providers/I18n/withOnlyLocales'
import en from './en.json'
import fr from './fr.json'

export const locales = {
const locales = {
en,
fr
}
Expand Down

0 comments on commit 7b54299

Please sign in to comment.