Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions apps/mobile/app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ module.exports = {
NSBluetoothPeripheralUsageDescription: '$(PRODUCT_NAME) will use Bluetooth to communicate with Ledger X.',
NSFaceIDUsageDescription: '$(PRODUCT_NAME) uses Face ID to secure access to your wallet.',
NSPhotoLibraryAddUsageDescription: '$(PRODUCT_NAME) will save QR codes to your photo library.',
NSPhotoLibraryUsageDescription: '$(PRODUCT_NAME) needs access to your photo library so you can pick a contact photo.',
NSLocationWhenInUseUsageDescription: '$(PRODUCT_NAME) needs access to your location for Bluetooth communication with Ledger devices.',
LSApplicationQueriesSchemes: ['itms-apps'],
UIRequiredDeviceCapabilities: ['arm64'],
Expand Down Expand Up @@ -237,6 +238,14 @@ module.exports = {
enableCodeScanner: true,
},
],
// Image picker for contact photos
[
'expo-image-picker',
{
photosPermission: 'Pera needs access to your photo library so you can pick a contact photo.',
cameraPermission: 'Pera needs access to your camera.',
},
],
// Note: The following packages are autolinked and don't require config plugins:
// - expo-sqlite
// - react-native-mmkv
Expand Down
10 changes: 10 additions & 0 deletions apps/mobile/assets/icons/contacts.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion apps/mobile/assets/icons/moon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions apps/mobile/assets/icons/pen-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"expo-font": "~55.0.6",
"expo-haptics": "^55.0.14",
"expo-image": "^55.0.8",
"expo-image-picker": "55.0.18",
"expo-linear-gradient": "^55.0.13",
"expo-localization": "^55.0.13",
"expo-media-library": "^55.0.14",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
Copyright 2022-2025 Pera Wallet, LDA
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
*/

import {
PWBottomSheet,
PWButton,
PWIcon,
PWText,
PWView,
} from '@components/core'
import { useStyles } from './styles'

import type { IconName, PWButtonProps, PWIconVariant } from '@components/core'

/**
* Generic confirmation bottom sheet. Callers supply labels, the
* confirm/cancel handlers, and — if they want one — an icon. No default
* icon is rendered so the component doesn't quietly assume a destructive
* context. Button variants default to primary/secondary; override per
* callsite when the design diverges.
*/
export type ConfirmActionBottomSheetProps = {
isVisible: boolean
onClose: () => void
onConfirm: () => void
icon?: IconName
iconVariant?: PWIconVariant
title: string
message: string
confirmLabel: string
cancelLabel: string
confirmVariant?: PWButtonProps['variant']
cancelVariant?: PWButtonProps['variant']
testID?: string
}

export const ConfirmActionBottomSheet = ({
isVisible,
onClose,
onConfirm,
icon,
iconVariant = 'primary',
title,
message,
confirmLabel,
cancelLabel,
confirmVariant = 'primary',
cancelVariant = 'secondary',
testID,
}: ConfirmActionBottomSheetProps) => {
const styles = useStyles()

return (
<PWBottomSheet
isVisible={isVisible}
onBackdropPress={onClose}
innerContainerStyle={styles.container}
enablePanDownToClose
enableContentPanningGesture
testID={testID}
>
{!!icon && (
<PWIcon
name={icon}
variant={iconVariant}
size='xxl'
style={styles.icon}
/>
)}
<PWText variant='h3'>{title}</PWText>
<PWText style={styles.message}>{message}</PWText>
<PWView style={styles.actions}>
<PWButton
variant={confirmVariant}
title={confirmLabel}
onPress={onConfirm}
/>
<PWButton
variant={cancelVariant}
title={cancelLabel}
onPress={onClose}
/>
</PWView>
</PWBottomSheet>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Copyright 2022-2025 Pera Wallet, LDA
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
*/

import { fireEvent, render, screen } from '@test-utils/render'
import { describe, it, expect, vi } from 'vitest'
import { ConfirmActionBottomSheet } from '../ConfirmActionBottomSheet'

describe('ConfirmActionBottomSheet', () => {
const baseProps = {
isVisible: true,
title: 'Delete contact',
message: 'Are you sure you want to delete this contact?',
confirmLabel: 'Yes, delete contact',
cancelLabel: 'Keep it',
}

it('renders the title, message and actions when visible', () => {
render(
<ConfirmActionBottomSheet
{...baseProps}
onClose={vi.fn()}
onConfirm={vi.fn()}
/>,
)
expect(screen.getByText(baseProps.title)).toBeTruthy()
expect(screen.getByText(baseProps.message)).toBeTruthy()
expect(screen.getByText(baseProps.confirmLabel)).toBeTruthy()
expect(screen.getByText(baseProps.cancelLabel)).toBeTruthy()
})

it('calls onConfirm when the confirm button is pressed', () => {
const onConfirm = vi.fn()
render(
<ConfirmActionBottomSheet
{...baseProps}
onClose={vi.fn()}
onConfirm={onConfirm}
/>,
)
fireEvent.click(screen.getByText(baseProps.confirmLabel))
expect(onConfirm).toHaveBeenCalledTimes(1)
})

it('calls onClose when the cancel button is pressed', () => {
const onClose = vi.fn()
render(
<ConfirmActionBottomSheet
{...baseProps}
onClose={onClose}
onConfirm={vi.fn()}
/>,
)
fireEvent.click(screen.getByText(baseProps.cancelLabel))
expect(onClose).toHaveBeenCalledTimes(1)
})
})
14 changes: 14 additions & 0 deletions apps/mobile/src/components/ConfirmActionBottomSheet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
Copyright 2022-2025 Pera Wallet, LDA
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
*/

export { ConfirmActionBottomSheet } from './ConfirmActionBottomSheet'
export type { ConfirmActionBottomSheetProps } from './ConfirmActionBottomSheet'
35 changes: 35 additions & 0 deletions apps/mobile/src/components/ConfirmActionBottomSheet/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
Copyright 2022-2025 Pera Wallet, LDA
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
*/

import { makeStyles } from '@rneui/themed'

export const useStyles = makeStyles(theme => ({
container: {
paddingVertical: theme.spacing.xxl,
alignItems: 'center',
},
icon: {
marginBottom: theme.spacing.lg,
},
message: {
textAlign: 'center',
paddingHorizontal: theme.spacing.lg,
paddingVertical: theme.spacing.md,
color: theme.colors.textGray,
},
actions: {
width: '100%',
paddingHorizontal: theme.spacing.lg,
paddingTop: theme.spacing.md,
gap: theme.spacing.sm,
},
}))
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
Copyright 2022-2025 Pera Wallet, LDA
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
*/

import { ConfirmActionBottomSheet } from '@components/ConfirmActionBottomSheet'
import { useLanguage } from '@hooks/useLanguage'

export type PhotoPermissionDeniedSheetProps = {
isVisible: boolean
onClose: () => void
onOpenSettings: () => void
}

/**
* Bottom sheet shown when the photo-library permission has been denied
* permanently (iOS / legacy Android). Guides the user to the system
* Settings app to grant access. Owns its own i18n copy so callers only
* have to wire visibility + handlers.
*/
export const PhotoPermissionDeniedSheet = ({
isVisible,
onClose,
onOpenSettings,
}: PhotoPermissionDeniedSheetProps) => {
const { t } = useLanguage()

return (
<ConfirmActionBottomSheet
isVisible={isVisible}
onClose={onClose}
onConfirm={onOpenSettings}
title={t('image_picker.permission_title')}
message={t('image_picker.permission_body')}
confirmLabel={t('image_picker.open_settings.label')}
cancelLabel={t('common.cancel.label')}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
Copyright 2022-2025 Pera Wallet, LDA
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
*/

import { fireEvent, render, screen } from '@test-utils/render'
import { describe, it, expect, vi } from 'vitest'
import { PhotoPermissionDeniedSheet } from '../PhotoPermissionDeniedSheet'

describe('PhotoPermissionDeniedSheet', () => {
it('renders the permission copy when visible', () => {
render(
<PhotoPermissionDeniedSheet
isVisible
onClose={vi.fn()}
onOpenSettings={vi.fn()}
/>,
)
expect(screen.getByText('image_picker.permission_title')).toBeTruthy()
expect(
screen.getByText('image_picker.open_settings.label'),
).toBeTruthy()
})

it('invokes onOpenSettings when the confirm button is pressed', () => {
const onOpenSettings = vi.fn()
render(
<PhotoPermissionDeniedSheet
isVisible
onClose={vi.fn()}
onOpenSettings={onOpenSettings}
/>,
)
fireEvent.click(screen.getByText('image_picker.open_settings.label'))
expect(onOpenSettings).toHaveBeenCalledTimes(1)
})

it('invokes onClose when the cancel button is pressed', () => {
const onClose = vi.fn()
render(
<PhotoPermissionDeniedSheet
isVisible
onClose={onClose}
onOpenSettings={vi.fn()}
/>,
)
fireEvent.click(screen.getByText('common.cancel.label'))
expect(onClose).toHaveBeenCalledTimes(1)
})
})
14 changes: 14 additions & 0 deletions apps/mobile/src/components/PhotoPermissionDeniedSheet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
Copyright 2022-2025 Pera Wallet, LDA
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
*/

export { PhotoPermissionDeniedSheet } from './PhotoPermissionDeniedSheet'
export type { PhotoPermissionDeniedSheetProps } from './PhotoPermissionDeniedSheet'
Loading
Loading