Skip to content

Commit 3e05166

Browse files
authored
Merge pull request #3481 from ecency/bugfix/hivesigner-broadcast-confirmation
hivesigner: carry the broadcast result back from the signing WebView
2 parents f0d16e2 + df29cbd commit 3e05166

4 files changed

Lines changed: 144 additions & 27 deletions

File tree

src/components/hiveSignerModal/hiveSignerModal.tsx

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useIntl } from 'react-intl';
33
import WebView from 'react-native-webview';
44
import { Platform, View } from 'react-native';
55
import { SafeAreaView } from 'react-native-safe-area-context';
6-
import type { Operation } from '@ecency/sdk';
6+
import type { Operation, TransactionConfirmation } from '@ecency/sdk';
77
import { hsOptions } from '../../constants/hsOptions';
88
import styles from './hiveSignerModal.styles';
99
import { ModalHeader } from '../modalHeader';
@@ -12,6 +12,7 @@ import { StatusContent } from '../hiveAuthModal/children/statusContent';
1212
import AUTH_TYPE from '../../constants/authType';
1313
import { useAppSelector } from '../../hooks';
1414
import { selectCurrentAccount } from '../../redux/selectors';
15+
import { parseHiveSignerSignResult } from '../../utils/hiveSignerCallback';
1516

1617
export const HiveSignerModal = ({ route, navigation }: any) => {
1718
const intl = useIntl();
@@ -104,30 +105,24 @@ export const HiveSignerModal = ({ route, navigation }: any) => {
104105
return;
105106
}
106107

107-
// Parse URL robustly to detect success
108-
let isSuccess = false;
109-
try {
110-
if (url.includes('/sign/success')) {
111-
isSuccess = true;
112-
} else {
113-
const parsedUrl = new URL(url);
114-
const successParam = parsedUrl.searchParams.get('success');
115-
if (successParam === 'true') {
116-
isSuccess = true;
117-
}
118-
}
119-
} catch (error) {
120-
// If URL parsing fails, fall back to includes check
121-
if (url.includes('?success=true')) {
122-
isSuccess = true;
123-
}
124-
}
108+
const result = parseHiveSignerSignResult(url, hsOptions.redirect_uri);
125109

126-
if (isSuccess) {
110+
if (result) {
127111
// Mark success as handled to prevent duplicate calls
128112
successHandledRef.current = true;
129-
// Transaction was successfully signed
130-
onSuccessRef.current?.();
113+
// Transaction was successfully signed. Pass the confirmation through when the
114+
// callback carried one: the SDK gates recordActivity on the transaction id, so
115+
// without it the action earns nothing and never reaches quest progress.
116+
onSuccessRef.current?.(
117+
result.id
118+
? ({
119+
id: result.id,
120+
block_num: result.blockNum,
121+
trx_num: result.trxNum,
122+
expired: false,
123+
} as unknown as TransactionConfirmation)
124+
: undefined,
125+
);
131126
navigation.goBack();
132127
}
133128
};
@@ -155,7 +150,15 @@ export const HiveSignerModal = ({ route, navigation }: any) => {
155150
return null;
156151
}
157152

158-
const _hsUri = `${hsOptions.base_url}${hiveuri.substring(7)}`;
153+
// Ask HiveSigner to hand the broadcast result back. Without a redirect_uri (or a
154+
// callback baked into the uri) it simply stops on its own success page, which is why
155+
// the signing path never had a transaction id to record the point activity with. The
156+
// loopback URI is the one already registered for this client and used by the OAuth
157+
// login flow: the WebView never actually loads it, it only reads the query params off
158+
// the navigation attempt.
159+
const _hsUri = `${hsOptions.base_url}${hiveuri.substring(7)}${
160+
hiveuri.includes('?') ? '&' : '?'
161+
}redirect_uri=${encodeURIComponent(hsOptions.redirect_uri)}`;
159162

160163
// Render HiveSigner WebView for HiveSigner operations
161164
return (

src/providers/sdk/mobilePlatformAdapter.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,11 @@ export function createMobilePlatformAdapter(params: MobilePlatformAdapterParams)
194194
params: {
195195
hiveuri: encodedUri,
196196
opsArray: ops,
197-
onSuccess: () => resolve({} as TransactionConfirmation),
197+
// The modal hands back a real confirmation when HiveSigner's callback
198+
// carried one. Resolving an empty object loses the transaction id, and
199+
// the SDK gates recordActivity on it, so the action earns nothing.
200+
onSuccess: (confirmation?: TransactionConfirmation) =>
201+
resolve(confirmation ?? ({} as TransactionConfirmation)),
198202
onClose: () => reject(new Error('HiveSigner signing cancelled')),
199203
},
200204
});
@@ -221,9 +225,11 @@ export function createMobilePlatformAdapter(params: MobilePlatformAdapterParams)
221225
params: {
222226
hiveuri: encodedUri,
223227
opsArray: ops,
224-
onSuccess: () => {
225-
// HiveSigner WebView confirms via URL redirect, no tx confirmation returned
226-
resolve({} as TransactionConfirmation);
228+
onSuccess: (confirmation?: TransactionConfirmation) => {
229+
// The modal reads the broadcast result off HiveSigner's callback. Falling
230+
// back to an empty object keeps the old behaviour when no id came through,
231+
// but with one the SDK can finally record the point activity.
232+
resolve(confirmation ?? ({} as TransactionConfirmation));
227233
},
228234
onClose: () => {
229235
reject(new Error('HiveSigner signing cancelled'));
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { parseHiveSignerSignResult } from './hiveSignerCallback';
2+
3+
const REDIRECT = 'http://127.0.0.1:3000/auth';
4+
5+
describe('parseHiveSignerSignResult', () => {
6+
it('reads the transaction id off the callback', () => {
7+
const result = parseHiveSignerSignResult(
8+
`${REDIRECT}?id=6f7c217c2f5a09a5b84fc73a33c5178d236b7059&block=108922746&txn=3&sig=abc`,
9+
REDIRECT,
10+
);
11+
12+
expect(result).toEqual({
13+
id: '6f7c217c2f5a09a5b84fc73a33c5178d236b7059',
14+
blockNum: 108922746,
15+
trxNum: 3,
16+
});
17+
});
18+
19+
it('keeps the id when block and txn are missing', () => {
20+
const result = parseHiveSignerSignResult(`${REDIRECT}?id=abc123`, REDIRECT);
21+
22+
expect(result).toEqual({ id: 'abc123', blockNum: undefined, trxNum: undefined });
23+
});
24+
25+
it('treats a callback without an id as nothing happened', () => {
26+
// Not a success we can act on, and reporting one would resolve the broadcast with
27+
// no transaction id, which is the bug this parsing exists to close.
28+
expect(parseHiveSignerSignResult(`${REDIRECT}?sig=abc`, REDIRECT)).toBeNull();
29+
});
30+
31+
it('still recognises the legacy success shapes, without an id', () => {
32+
expect(parseHiveSignerSignResult('https://hivesigner.com/sign/success', REDIRECT)).toEqual({});
33+
expect(parseHiveSignerSignResult('https://hivesigner.com/x?success=true', REDIRECT)).toEqual(
34+
{},
35+
);
36+
});
37+
38+
it('says nothing about an ordinary navigation', () => {
39+
expect(parseHiveSignerSignResult('https://hivesigner.com/sign/op/abc', REDIRECT)).toBeNull();
40+
expect(parseHiveSignerSignResult('https://hivesigner.com/login', REDIRECT)).toBeNull();
41+
expect(parseHiveSignerSignResult(undefined, REDIRECT)).toBeNull();
42+
expect(parseHiveSignerSignResult('', REDIRECT)).toBeNull();
43+
});
44+
});

src/utils/hiveSignerCallback.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
export interface HiveSignerSignResult {
2+
/** The signed transaction id, when HiveSigner handed one back. */
3+
id?: string;
4+
blockNum?: number;
5+
trxNum?: number;
6+
}
7+
8+
/**
9+
* Read the outcome of a HiveSigner hot-signing session off a WebView navigation.
10+
*
11+
* HiveSigner resolves the configured callback with `sig`, `id`, `block` and `txn`
12+
* (see its sign page), so the callback navigation is where the transaction id lives.
13+
* The SDK gates `recordActivity` on that id, so losing it means the action earns no
14+
* points and never shows up in quest progress.
15+
*
16+
* Returns null when the url says nothing about the outcome. The two legacy shapes are
17+
* still recognised so a session that somehow lands on them is not treated as a
18+
* cancellation, they just carry no transaction id.
19+
*/
20+
export const parseHiveSignerSignResult = (
21+
url: string | undefined | null,
22+
redirectUri: string,
23+
): HiveSignerSignResult | null => {
24+
if (!url) {
25+
return null;
26+
}
27+
28+
if (url.startsWith(redirectUri)) {
29+
try {
30+
const params = new URL(url).searchParams;
31+
const id = params.get('id');
32+
if (!id) {
33+
return null;
34+
}
35+
36+
const blockNum = Number(params.get('block'));
37+
const trxNum = Number(params.get('txn'));
38+
39+
return {
40+
id,
41+
blockNum: Number.isFinite(blockNum) && blockNum > 0 ? blockNum : undefined,
42+
trxNum: Number.isFinite(trxNum) && trxNum > 0 ? trxNum : undefined,
43+
};
44+
} catch (error) {
45+
return null;
46+
}
47+
}
48+
49+
if (url.includes('/sign/success')) {
50+
return {};
51+
}
52+
53+
try {
54+
if (new URL(url).searchParams.get('success') === 'true') {
55+
return {};
56+
}
57+
} catch (error) {
58+
if (url.includes('?success=true')) {
59+
return {};
60+
}
61+
}
62+
63+
return null;
64+
};

0 commit comments

Comments
 (0)