Skip to content

Commit a0ae087

Browse files
committed
VK user init data: utm, info
1 parent e61f58f commit a0ae087

File tree

6 files changed

+83
-10
lines changed

6 files changed

+83
-10
lines changed

.github/workflows/deploy.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ jobs:
106106
port: ${{ secrets.PORT }}
107107
username: ${{ secrets.USER }}
108108
key: ${{ secrets.SSH }}
109+
command_timeout: 30m
109110
# script_stop: true
110111
script: |
111112
# set -euo pipefail
@@ -201,7 +202,7 @@ jobs:
201202
"${STACK}_${service}"
202203
done
203204
204-
#
205+
echo "Clean"
205206
yes | sudo docker system prune -a
206207
207208
# check services

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ The "tasks" feature is a reward checklist that grants users inner coins after ve
4747
## Referrals (Frens)
4848
- Backend: `POST /users/frens/` returns the current user's frens list (sorted by balance) with relation labels and `referral_link`.
4949
- Referral key: `UserLocal.link` is generated via `encrypt(user.id, 8)` and decoded with `decrypt()` in `routes/users/auth.update_utm` to resolve referrers.
50+
- VK Mini Apps: share links should use `vk_ref=<referral>` so the frontend can map it to `utm` on session init.
5051
- Frontend: `web/src/app/[locale]/social/page.tsx` renders the frens list and uses `navigation.frens` + `social.*` i18n; mobile bottom bar includes the frens entry linking to `/social`.
5152

5253
## Environments

web/src/features/auth/api/authApi.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ export async function loginWithTelegramAppApi(data: TelegramAppAuthRequest): Pro
4141
export interface VkAppAuthRequest {
4242
url: string;
4343
utm?: string | null;
44+
name?: string | null;
45+
surname?: string | null;
46+
image?: string | null;
47+
mail?: string | null;
4448
}
4549

4650
export async function loginWithVkAppApi(data: VkAppAuthRequest): Promise<AuthUser> {

web/src/features/auth/components/VkAuthInitializer.tsx

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,53 @@ import { useAppDispatch, useAppSelector } from '@/shared/stores/store';
88
import { isVkMiniApp } from '@/shared/lib/vk';
99
import { loginWithVkApp, selectAuthUser } from '../stores/authSlice';
1010

11+
type VkUserInfo = {
12+
first_name?: string;
13+
last_name?: string;
14+
photo_200?: string;
15+
photo_200_orig?: string;
16+
photo_100?: string;
17+
};
18+
19+
const getVkUtm = () => {
20+
if (typeof window === 'undefined') return null;
21+
const params = new URLSearchParams(window.location.search);
22+
return params.get('utm') || params.get('vk_ref') || null;
23+
};
24+
25+
const getVkEmail = () => {
26+
if (typeof window === 'undefined') return null;
27+
const params = new URLSearchParams(window.location.search);
28+
return params.get('vk_email') || null;
29+
};
30+
31+
const getVkUserInfo = async (): Promise<VkUserInfo | null> => {
32+
if (typeof window === 'undefined') return null;
33+
const bridge = window.vkBridge;
34+
if (!bridge?.send) return null;
35+
36+
const supportsAsync = (bridge as unknown as { supportsAsync?: (method: string) => Promise<boolean> }).supportsAsync;
37+
if (typeof supportsAsync === 'function') {
38+
try {
39+
const supported = await supportsAsync('VKWebAppGetUserInfo');
40+
if (!supported) return null;
41+
} catch {
42+
// Ignore capability detection errors.
43+
}
44+
}
45+
46+
try {
47+
const response = await bridge.send('VKWebAppGetUserInfo');
48+
if (response && typeof response === 'object') {
49+
return response as VkUserInfo;
50+
}
51+
} catch {
52+
// Ignore VK bridge errors.
53+
}
54+
55+
return null;
56+
};
57+
1158
export default function VkAuthInitializer() {
1259
const dispatch = useAppDispatch();
1360
const { error: showError } = useToastActions();
@@ -28,8 +75,25 @@ export default function VkAuthInitializer() {
2875

2976
hasAttempted.current = true;
3077
inFlightRef.current = true;
31-
dispatch(loginWithVkApp({ url, utm }))
32-
.unwrap()
78+
const runAuth = async () => {
79+
const resolvedUtm = utm || getVkUtm();
80+
const userInfo = await getVkUserInfo();
81+
const image = userInfo?.photo_200_orig || userInfo?.photo_200 || userInfo?.photo_100 || null;
82+
const mail = getVkEmail();
83+
84+
return dispatch(
85+
loginWithVkApp({
86+
url,
87+
utm: resolvedUtm,
88+
name: userInfo?.first_name,
89+
surname: userInfo?.last_name,
90+
image,
91+
mail,
92+
})
93+
).unwrap();
94+
};
95+
96+
runAuth()
3397
.catch((err) => {
3498
showError(formatApiErrorMessage(err, tSystem('server_error')));
3599
})

web/src/features/session/components/SessionInitializer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default function SessionInitializer() {
1717
const authToken = useAppSelector((state) => state.session.authToken);
1818
const { error: showError } = useToastActions();
1919

20-
const utmParam = useMemo(() => searchParams?.get('utm') || null, [searchParams]);
20+
const utmParam = useMemo(() => searchParams?.get('utm') || searchParams?.get('vk_ref') || null, [searchParams]);
2121

2222
useEffect(() => {
2323
if (utmParam) {

web/src/widgets/social/ui/SocialPage.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const buildVkReferralUrl = (referralKey: string) => {
4949
}
5050

5151
const url = new URL(`https://vk.com/app${appId}`);
52-
url.searchParams.set('utm', referralKey);
52+
url.searchParams.set('vk_ref', referralKey);
5353
return url.toString();
5454
};
5555

@@ -115,7 +115,7 @@ const openTelegramShareMessage = (messageId: string) => {
115115
return true;
116116
};
117117

118-
const openVkShare = async ({ url, text }: { url: string; text?: string }) => {
118+
const openVkShare = async ({ url, text, forceLink = false }: { url: string; text?: string; forceLink?: boolean }) => {
119119
if (typeof window === 'undefined') return false;
120120
const bridge = window.vkBridge;
121121
if (!bridge?.send) return false;
@@ -125,8 +125,12 @@ const openVkShare = async ({ url, text }: { url: string; text?: string }) => {
125125
? (bridge as unknown as { isWebView: () => boolean }).isWebView()
126126
: false;
127127

128+
if (forceLink && !(await supportsVkMethod('VKWebAppShare'))) {
129+
return false;
130+
}
131+
128132
// New invite dialog (mobile webview only; crashes on desktop/messenger if unsupported).
129-
if (isWebView && (await supportsVkMethod('VKWebAppShowInviteBox'))) {
133+
if (!forceLink && isWebView && (await supportsVkMethod('VKWebAppShowInviteBox'))) {
130134
const inviteParams = text ? { message: text } : undefined;
131135
try {
132136
await bridge.send('VKWebAppShowInviteBox', inviteParams);
@@ -144,7 +148,7 @@ const openVkShare = async ({ url, text }: { url: string; text?: string }) => {
144148
}
145149

146150
// VK recommend sheet (if available) - no custom text support.
147-
if (await supportsVkMethod('VKWebAppRecommend')) {
151+
if (!forceLink && (await supportsVkMethod('VKWebAppRecommend'))) {
148152
try {
149153
await bridge.send('VKWebAppRecommend');
150154
return true;
@@ -439,8 +443,7 @@ export default function SocialPage() {
439443
}
440444

441445
if (network === 'vk' || hasVkBridge) {
442-
const shareMessage = text ? `${text} ${url}` : url;
443-
if (await openVkShare({ url, text: shareMessage })) return;
446+
if (await openVkShare({ url, text, forceLink: true })) return;
444447
}
445448

446449
if (network === 'max') {

0 commit comments

Comments
 (0)