Skip to content

Commit

Permalink
added share feature (Receive screen only for now), enhanced Collapsed…
Browse files Browse the repository at this point in the history
…QR with icon-only mode
  • Loading branch information
myxmaster committed Jan 12, 2025
1 parent f1eb3f6 commit 1421d1b
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 19 deletions.
109 changes: 105 additions & 4 deletions components/CollapsedQR.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import {
TouchableOpacity,
TouchableWithoutFeedback
} from 'react-native';
import QRCode from 'react-native-qrcode-svg';

import QRCode, { QRCodeProps } from 'react-native-qrcode-svg';
import HCESession, { NFCContentType, NFCTagType4 } from 'react-native-hce';

import Amount from './Amount';
import Button from './Button';
import CopyButton from './CopyButton';
import ShareButton from './ShareButton';
import { localeString } from './../utils/LocaleUtils';
import { themeColor } from './../utils/ThemeUtils';
import Touchable from './Touchable';
Expand All @@ -26,11 +27,36 @@ const defaultLogoWhite = require('../assets/images/icon-white.png');

let simulation: any;

type QRCodeElement = React.ElementRef<typeof QRCode>;

interface ExtendedQRCodeProps
extends QRCodeProps,
React.RefAttributes<QRCodeElement> {
onLoad?: () => void;
parent?: CollapsedQR;
}

interface ValueTextProps {
value: string;
truncateLongValue?: boolean;
}

// Custom QR code component that forwards refs and handles component readiness
// Sets qrReady state on first valid component mount to prevent remounting cycles
const ForwardedQRCode = React.forwardRef<QRCodeElement, ExtendedQRCodeProps>(
(props, ref) => (
<QRCode
{...props}
getRef={(c) => {
if (c && c.toDataURL && !(ref as any).current) {
(ref as any).current = c;
props.parent?.setState({ qrReady: true });
}
}}
/>
)
) as React.FC<ExtendedQRCodeProps>;

function ValueText({ value, truncateLongValue }: ValueTextProps) {
const [state, setState] = React.useState<{
numberOfValueLines: number | undefined;
Expand Down Expand Up @@ -64,6 +90,9 @@ interface CollapsedQRProps {
collapseText?: string;
copyText?: string;
copyValue?: string;
copyIconContainerStyle?: any;
showShare?: boolean;
iconOnly?: boolean;
hideText?: boolean;
expanded?: boolean;
textBottom?: boolean;
Expand All @@ -78,16 +107,22 @@ interface CollapsedQRState {
collapsed: boolean;
nfcBroadcast: boolean;
enlargeQR: boolean;
tempQRRef: React.RefObject<QRCodeElement> | null;
qrReady: boolean;
}

export default class CollapsedQR extends React.Component<
CollapsedQRProps,
CollapsedQRState
> {
qrRef = React.createRef<QRCodeElement>();

state = {
collapsed: this.props.expanded ? false : true,
nfcBroadcast: false,
enlargeQR: false
enlargeQR: false,
tempQRRef: null,
qrReady: false
};

componentWillUnmount() {
Expand Down Expand Up @@ -134,13 +169,16 @@ export default class CollapsedQR extends React.Component<
};

render() {
const { collapsed, nfcBroadcast, enlargeQR } = this.state;
const { collapsed, nfcBroadcast, enlargeQR, tempQRRef } = this.state;
const {
value,
showText,
copyText,
copyValue,
collapseText,
copyIconContainerStyle,
showShare,
iconOnly,
hideText,
expanded,
textBottom,
Expand All @@ -151,8 +189,42 @@ export default class CollapsedQR extends React.Component<

const { width, height } = Dimensions.get('window');

// Creates a temporary QR code for sharing and waits for component to be ready
// Returns a promise that resolves when QR is fully rendered and ready to be captured
const handleShare = () =>
new Promise<void>((resolve) => {
const tempRef = React.createRef<QRCodeElement>();
this.setState({ tempQRRef: tempRef, qrReady: false }, () => {
const checkReady = () => {
if (this.state.qrReady) {
resolve();
} else {
requestAnimationFrame(checkReady);
}
};
checkReady();
});
});

return (
<React.Fragment>
{/* Temporary QR for sharing */}
{tempQRRef && (
<View style={{ height: 0, width: 0, overflow: 'hidden' }}>
<ForwardedQRCode
ref={tempQRRef}
value={value}
size={800}
logo={defaultLogo}
backgroundColor={'white'}
logoBackgroundColor={'white'}
logoMargin={10}
quietZone={40}
parent={this}
/>
</View>
)}

{satAmount != null && this.props.displayAmount && (
<View
style={{
Expand Down Expand Up @@ -277,7 +349,36 @@ export default class CollapsedQR extends React.Component<
onPress={() => this.toggleCollapse()}
/>
)}
<CopyButton copyValue={copyValue || value} title={copyText} />
{showShare ? (
<View
style={{
flexDirection: 'row',
justifyContent: 'center'
}}
>
<CopyButton
copyIconContainerStyle={copyIconContainerStyle}
copyValue={copyValue || value}
title={copyText}
iconOnly={iconOnly}
/>
<ShareButton
value={copyValue || value}
qrRef={tempQRRef}
iconOnly={iconOnly}
onPress={handleShare}
onShareComplete={() =>
this.setState({ tempQRRef: null })
}
/>
</View>
) : (
<CopyButton
copyValue={copyValue || value}
title={copyText}
iconOnly={iconOnly}
/>
)}
{Platform.OS === 'android' && this.props.nfcSupported && (
<Button
title={
Expand Down
99 changes: 99 additions & 0 deletions components/ShareButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as React from 'react';
import { Platform, TouchableOpacity } from 'react-native';

import QRCode from 'react-native-qrcode-svg';
import { captureRef } from 'react-native-view-shot';
import Share from 'react-native-share';
import { Icon } from 'react-native-elements';

import Button from './../components/Button';

import { localeString } from './../utils/LocaleUtils';
import { themeColor } from './../utils/ThemeUtils';

type QRCodeElement = React.ElementRef<typeof QRCode>;

interface ShareButtonProps {
value: string;
qrRef: React.RefObject<QRCodeElement> | null;
title?: string;
icon?: any;
noUppercase?: boolean;
iconOnly?: boolean;
onPress: () => Promise<void>;
onShareComplete?: () => void;
}

export default class ShareButton extends React.Component<ShareButtonProps> {
handlePress = async () => {
const { onPress } = this.props;
await onPress();
await this.shareContent();
};

shareContent = async () => {
const { value, qrRef, onShareComplete } = this.props;
try {
if (!qrRef?.current) {
return;
}

const svgElement = qrRef.current;
const base64Data = await captureRef(svgElement, {
format: 'png',
quality: 1
});

await Share.open({
message: value,
url: base64Data
});
} catch (error) {
// Share API throws error when share sheet closes, regardless of success
console.log('Error in shareContent:', error);
} finally {
onShareComplete?.();
}
};

render() {
const { title, icon, noUppercase, iconOnly } = this.props;

if (iconOnly) {
return (
<TouchableOpacity
// "padding: 5" leads to a larger area where users can click on
style={{ padding: 5 }}
onPress={this.handlePress}
>
<Icon
name={'share'}
size={27}
color={themeColor('secondaryText')}
/>
</TouchableOpacity>
);
}

return (
<Button
title={title || localeString('general.share')}
icon={
icon
? icon
: {
name: 'share',
size: 25
}
}
containerStyle={{
marginTop: 10,
marginBottom: Platform.OS === 'android' ? 0 : 20
}}
onPress={this.handlePress}
secondary
noUppercase={noUppercase}
/>
);
}
}
1 change: 1 addition & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"general.close": "Close",
"general.hide": "Hide",
"general.copy": "Copy",
"general.share": "Share",
"general.goBack": "Go Back",
"general.lightning": "Lightning",
"general.onchain": "On-chain",
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"react-native-safe-area-context": "4.10.1",
"react-native-screens": "3.31.1",
"react-native-securerandom": "1.0.1",
"react-native-share": "12.0.3",
"react-native-svg": "15.3.0",
"react-native-svg-transformer": "1.3.0",
"react-native-system-navigation-bar": "2.6.4",
Expand All @@ -128,6 +129,7 @@
"react-native-udp": "4.1.7",
"react-native-v8": "0.61.5-patch.4",
"react-native-vector-icons": "7.1.0",
"react-native-view-shot": "4.0.3",
"react-native-vision-camera": "4.3.2",
"readable-stream": "1.0.33",
"sha.js": "2.4.11",
Expand Down
40 changes: 25 additions & 15 deletions views/Receive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1851,9 +1851,11 @@ export default class Receive extends React.Component<
haveUnifiedInvoice && (
<CollapsedQR
value={unifiedInvoice || ''}
copyText={localeString(
'views.Receive.copyInvoice'
)}
iconOnly={true}
copyIconContainerStyle={{
marginRight: 40
}}
showShare={true}
expanded
textBottom
truncateLongValue
Expand Down Expand Up @@ -1881,9 +1883,11 @@ export default class Receive extends React.Component<
copyValue={
lnInvoiceCopyValue
}
copyText={localeString(
'views.Receive.copyInvoice'
)}
iconOnly={true}
copyIconContainerStyle={{
marginRight: 40
}}
showShare={true}
expanded
textBottom
truncateLongValue
Expand Down Expand Up @@ -1911,9 +1915,11 @@ export default class Receive extends React.Component<
copyValue={
btcAddressCopyValue
}
copyText={localeString(
'views.Receive.copyAddress'
)}
iconOnly={true}
copyIconContainerStyle={{
marginRight: 40
}}
showShare={true}
expanded
textBottom
truncateLongValue
Expand Down Expand Up @@ -1990,9 +1996,11 @@ export default class Receive extends React.Component<
lightningAddress && (
<CollapsedQR
value={`lightning:${lightningAddress}`}
copyText={localeString(
'views.Receive.copyAddress'
)}
iconOnly={true}
copyIconContainerStyle={{
marginRight: 40
}}
showShare={true}
expanded
textBottom
hideText
Expand Down Expand Up @@ -2023,9 +2031,11 @@ export default class Receive extends React.Component<
copyValue={
lnInvoiceCopyValue
}
copyText={localeString(
'views.Receive.copyInvoice'
)}
iconOnly={true}
copyIconContainerStyle={{
marginRight: 40
}}
showShare={true}
expanded
textBottom
truncateLongValue
Expand Down
Loading

0 comments on commit 1421d1b

Please sign in to comment.