Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve handling of declined portings #14

Merged
merged 7 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 64 additions & 63 deletions docs/porting-embed.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,19 @@ The ID of your Gigs Project.
Callback triggered when the number porting is completed.
`porting` - The whole porting object.

### Optional props

#### `onError`

`(error: Error) => unknown`
Callback triggered when there is an error initializing the porting form. The error may stem from an invalid session, in which case recovery can be attempted by generating and passing a new Connect session to the embed. Alternatively, the error may derive from a failed attempt to retrieve a subscription, in which case it is advisable to inform the user to try again later or to contact customer support for assistance.
`(error: Error, meta: Metadata) => unknown`

Callback triggered when the embed is in an error state. The `metadata` object includes a `code` field for the error, which gives more context about the error, and an optional `porting` field. The callback is triggered when:

- There is an error initializing the porting form (code `'initializationError'`). The error may stem from an invalid session, in which case recovery can be attempted by generating and passing a new Connect session to the embed. The `porting` object is passed if present.
- There is an error retrieving the authorization token (code `'tokenFetchingError'`).
- There is a failed attempt to retrieve a subscription (the code is passed from the [Gigs API Error codes](https://developers.gigs.com/docs/concepts/error-codes)). In this case it is advisable to inform the user to try again later or to contact customer support for assistance. The `error` is passed as an argument to the callback.
- There is an error fetching service providers in the donor providers form (`'providersNotFound'`).
- The porting has been declined. The `porting` object and the error code `portingDeclined` are passed as arguments to the callback.

### Optional props

#### `onInitialized`

Expand All @@ -73,11 +80,6 @@ Callback triggered when the embed form is loaded with a subscription and the rel
`(step: PortingStep) => unknown`
Callback that returns the current porting step. Useful to display custom headlines and information in your app depending on the step.

#### `onSupportRequested`

`() => unknown`
Callback triggered when the user has clicked the "Customer support" button. You can use it, for example, to open your own Customer support page.

#### `renderTitle`

`(step: PortingStep) => unknown`
Expand Down Expand Up @@ -210,7 +212,6 @@ Returns a React node representing the customized secondary button component.
```jsx
const titles: Record<string, string> = {
'protectionDisabling.button': 'Request Porting Again',
'portingDeclined.button': 'Contact Customer support',
}

<PortingEmbed
Expand All @@ -235,38 +236,38 @@ renderSecondaryButton={(name, onPress) => (

`(variant: 'error' | 'info', message: string) => React.ReactNode`
Render prop function that can be used to customize the rendering of alert banners in the embed.
Alert banners are used either to convey information or to display errors.
Alert banners are used either to convey information or to display errors.
The `'info'` type appears in the `holderDetails` and `address` steps as follows:

<img width="400" src="./holderDetailsInfo.png" alt="Account holder info alert banner">
<img width="400" src="./addressInfo.png" alt="Address info alert banner">

The `'error'` type appears whenever there is an error after submitting a porting step, or if the porting has been declined (see all the declined messages [here](https://github.com/gigs/embeds-react-native/blob/main/src/PortingEmbed/util/portingUtils.tsx#L96)).
The `'error'` type appears whenever there is an error after submitting a porting step, or if the porting has been declined (see all the declined messages [here](https://github.com/gigs/embeds-react-native/blob/main/src/PortingEmbed/util/portingUtils.tsx#L96)).

`variant` - The variant of the alert banner (either 'error' or 'info').
`message` - The message to be displayed in the alert banner.
Returns a React node representing the customized alert banner component.

```jsx
<PortingEmbed
//...
renderAlertBanner={(variant, message) => (
<View
style={[
{
backgroundColor: variant === 'error' ? '#FFE4E6' : 'white',
},
]}
>
<Text
style={{
color: variant === 'error' ? '#BE123C' : 'black',
}}
>
{message}
</Text>
</View>
)}
//...
renderAlertBanner={(variant, message) => (
<View
style={[
{
backgroundColor: variant === 'error' ? '#FFE4E6' : 'white',
},
]}
>
<Text
style={{
color: variant === 'error' ? '#BE123C' : 'black',
}}
>
{message}
</Text>
</View>
)}
/>
```

Expand Down Expand Up @@ -314,29 +315,31 @@ If no `renderPortingProtectionDisabledConfirmation` prop is passed to the compon

```jsx
<PortingEmbed
//...
renderPortingProtectionDisabledConfirmation={(onConfirm) => (
<View>
<Text>
After you have received confirmation that port protection has
been deactivated and your number is prepared for porting, kindly
inform us by clicking the button below to confirm.
</Text>
<Button title={'Confirm'} onPress={onConfirm} color={'blue'} />
</View>
)}
//...
renderPortingProtectionDisabledConfirmation={(onConfirm) => (
<View>
<Text>
After you have received confirmation that port protection has been
deactivated and your number is prepared for porting, kindly inform us by
clicking the button below to confirm.
</Text>
<Button title={'Confirm'} onPress={onConfirm} color={'blue'} />
</View>
)}
/>
```

#### `defaultTextFont`

The default font that the embed uses for all default components, i.e. all components that are not replaced by a corresponding render prop (for example all inputs will use the `defaultTextFont` if no `renderInput` component was passed to the embed).

```jsx
<PortingEmbed
//...
defaultTextFont='Custom-font'
//...
defaultTextFont="Custom-font"
/>
```

#### `renderProvidersDropdown`

Render prop function that can be used to customize the rendering of a component to select the current carrier.
Expand Down Expand Up @@ -372,25 +375,24 @@ renderProvidersDropdown={(name, providers, onChange) => (

#### Porting Embed props

| Prop | Type | Required | Description/Signature |
| -------------------- | ---------------------- | -------- | -------------------------------------------------------------------------------------------- |
| `connectSession` | Connect Session object | ✅ | A Connect Session object with an intent of type `completePorting`. |
| `project` | string | ✅ | Your project ID. |
| `onCompleted` | function | ✅ | `(porting: Porting) => unknown` |
| `onInitialized` | function | ❌ | `() => unknown` |
| `onLoaded` | function | ❌ | `() => unknown` |
| `onError` | function | ❌ | `(error: Error) => unknown` |
| `onSupportRequested` | function | ❌ | `() => unknown` |
| `onPortingStep` | function | ❌ | `(step: PortingStep) => unknown` |
| `renderTitle` | function | ❌ | `(step: PortingStep) => unknown` |
| `renderCheckbox` | function | ❌ | `(name: string, onChange: (value: string) => void, inputMode?: InputModeOptions) => unknown` |
| `renderPrimaryButton` | function | ❌ | `(onPress: () => void, name?: string, isSubmitting?: boolean, disabled?: boolean) => React.ReactNode` |
| `renderSecondaryButton` | function | ❌ | `(name: string, onPress: () => void) => React.ReactNode` |
| `renderAlertBanner` | function | ❌ | `(variant: 'error' | 'info', message: string) => React.ReactNode` |
| `renderDate` | function | ❌ | `(name: string, onChange: (value: string) => void) => React.ReactNode` |
| `renderPortingProtectionDisabledConfirmation` | function | ❌ | `(onConfirm: () => void) => React.ReactNode` |
| `renderProvidersDropdown` | function | ❌ | `(name: string, providers: {id: string; name: string;}[], onChange: (value: string) => void) => React.ReactNode` |
| `defaultTextFont` | string | ❌ | Custom font used in all default components. |
| Prop | Type | Required | Description/Signature |
| --------------------------------------------- | ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `connectSession` | Connect Session object | ✅ | A Connect Session object with an intent of type `completePorting`. |
| `project` | string | ✅ | Your project ID. |
| `onCompleted` | function | ✅ | `(porting: Porting) => unknown` |
| `onError` | function | ✅ | `(error: Error, meta: Metadata) => unknown` |
| `onInitialized` | function | ❌ | `() => unknown` |
| `onLoaded` | function | ❌ | `() => unknown` |
| `onPortingStep` | function | ❌ | `(step: PortingStep) => unknown` |
| `renderTitle` | function | ❌ | `(step: PortingStep) => unknown` |
| `renderCheckbox` | function | ❌ | `(name: string, onChange: (value: string) => void, inputMode?: InputModeOptions) => unknown` |
| `renderPrimaryButton` | function | ❌ | `(onPress: () => void, name?: string, isSubmitting?: boolean, disabled?: boolean) => React.ReactNode` |
| `renderSecondaryButton` | function | ❌ | `(name: string, onPress: () => void) => React.ReactNode` |
| `renderAlertBanner` | function | ❌ | `(variant: 'error' | 'info', message: string) => React.ReactNode` |
| `renderDate` | function | ❌ | `(name: string, onChange: (value: string) => void) => React.ReactNode` |
| `renderPortingProtectionDisabledConfirmation` | function | ❌ | `(onConfirm: () => void) => React.ReactNode` |
| `renderProvidersDropdown` | function | ❌ | `(name: string, providers: {id: string; name: string;}[], onChange: (value: string) => void) => React.ReactNode` |
| `defaultTextFont` | string | ❌ | Custom font used in all default components. |

#### Text

Expand All @@ -415,6 +417,5 @@ renderProvidersDropdown={(name, providers, onChange) => (
| `protectionDisabling.cancel` | Cancel |
| `portingInfoLink` | See Porting instructions |
| `protectionDisabling.button` | Request Porting Again |
| `portingDeclined.button` | Contact Customer support |
| `donorProvider` | N/A |
| `donorProvider.dropdown` | Donor providers dropdown |
27 changes: 16 additions & 11 deletions example/app/porting/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,20 @@ import {

import { PortingEmbed } from '../../../src'
import { PortingStep } from '../../../src/PortingEmbed/nextPortingStep'
import { Porting } from '../../../types/porting'

export default function PortingEmbedScreen() {
const [sessionJson, setSessionJson] = useState('')
const [connectSession, setConnectSession] = useState<any>(null)
const [isLoaded, setLoaded] = useState(false)
const [isCompleted, setCompleted] = useState(false)
const [clickedCustomerSupport, setClickedCustomerSupport] = useState(false)
const [portingStep, setPortingStep] = useState<PortingStep>()
const [isChecked, setChecked] = useState(false)
const [date, setDate] = useState<Date | undefined>(new Date())
const [currentProvider, setCurrentProvider] = useState<string | undefined>(
undefined
)
const [isPortingDeclined, setPortingDeclined] = useState<boolean>(false)

function handleSubmit() {
setConnectSession(JSON.parse(sessionJson))
Expand All @@ -41,11 +42,19 @@ export default function PortingEmbedScreen() {
setLoaded(true)
}, [])

const handleError = useCallback((_error: Error) => {
console.log('onError triggered')
setLoaded(false)
setConnectSession(null)
}, [])
type PortingEmbedError = 'portingDeclined' | string

const handleError = useCallback(
(_error: Error, meta: { code: PortingEmbedError; porting?: Porting }) => {
console.log('onError triggered')
if (meta.code && meta.code === 'portingDeclined') {
setPortingDeclined(true)
}
setLoaded(false)
setConnectSession(null)
},
[]
)

const handleInitialized = useCallback(() => {
console.log('onInitialized triggered, embed got a token')
Expand All @@ -71,7 +80,6 @@ export default function PortingEmbedScreen() {
'protectionDisabling.cancel': 'Cancel',
portingInfoLink: 'See Porting instructions',
'protectionDisabling.button': 'Request Porting Again',
'portingDeclined.button': 'Contact Customer support',
donorProvider: 'Current provider',
'donorProvider.dropdown': 'Select your current provider',
}
Expand Down Expand Up @@ -113,10 +121,8 @@ export default function PortingEmbedScreen() {
<View style={{ width: '100%', height: 1, backgroundColor: 'gray' }} />
{Boolean(!isLoaded && connectSession) && <Text>Loading...</Text>}
{isCompleted && <Text>The porting is submitted!</Text>}
{clickedCustomerSupport && (
<Text>User has requested customer support</Text>
)}
{portingStep && <Text>Current Porting step: {portingStep}</Text>}
{isPortingDeclined && <Text>The porting was declined.</Text>}
{!isCompleted && (
<PortingEmbed
project="dev"
Expand All @@ -125,7 +131,6 @@ export default function PortingEmbedScreen() {
onLoaded={handleLoaded}
onError={handleError}
onCompleted={() => setCompleted(true)}
onSupportRequested={() => setClickedCustomerSupport(true)}
onPortingStep={(step) => setPortingStep(step)}
defaultTextFont="Satoshi-Regular"
renderTitle={(step) =>
Expand Down
3 changes: 2 additions & 1 deletion src/PortingEmbed/CustomOptionsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { createContext, ReactNode, useContext } from 'react'
import { InputModeOptions } from 'react-native'

import { PortingStep } from './nextPortingStep'
import { Metadata } from './PortingEmbed'

type EmbedOptions = {
onError?: (error: Error) => unknown
onError: (error: Error, meta: Metadata) => unknown
3nvi marked this conversation as resolved.
Show resolved Hide resolved
renderTitle?: (step: PortingStep) => React.ReactNode
renderInput?: (
name: string,
Expand Down
18 changes: 14 additions & 4 deletions src/PortingEmbed/PortingEmbed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,29 @@ import { InputModeOptions } from 'react-native'

import { Porting } from '../../types/porting'
import { ConnectSessionProvider } from '../core/ConnectSessionProvider'
import { ApiErrorType } from '../data/api'
import { CustomOptionsProvider } from './CustomOptionsProvider'
import { PortingStep } from './nextPortingStep'
import { PortingFormContainer } from './PortingFormContainer'

export type Metadata = {
code:
| 'portingDeclined'
| 'tokenFetchingError'
| 'initializationError'
| 'providersNotFound'
| 'unexpectedError'
| ApiErrorType
porting?: Porting
}

type Props = {
connectSession?: unknown
project: string
onLoaded?: () => unknown
onInitialized?: () => unknown
onError?: (error: Error) => unknown
onError: (error: Error, meta: Metadata) => unknown
onCompleted: (porting: Porting) => unknown
onSupportRequested?: () => unknown
onPortingStep?: (portingStep: PortingStep) => unknown
renderTitle?: (step: PortingStep) => React.ReactNode
renderInput?: (
Expand Down Expand Up @@ -56,7 +67,6 @@ export function PortingEmbed({
onLoaded,
onError,
onCompleted,
onSupportRequested,
onPortingStep,
renderTitle,
renderInput,
Expand Down Expand Up @@ -90,12 +100,12 @@ export function PortingEmbed({
}
defaultTextFont={defaultTextFont}
renderProvidersDropdown={renderProvidersDropdown}
onError={onError}
>
<PortingFormContainer
onLoaded={onLoaded}
onError={onError}
onCompleted={onCompleted}
onSupportRequested={onSupportRequested}
onPortingStep={onPortingStep}
/>
</CustomOptionsProvider>
Expand Down
Loading
Loading