fix: 对齐首充双倍展示状态
This commit is contained in:
@@ -26,6 +26,8 @@
|
|||||||
|
|
||||||
泥点充值固定为 `¥6 / ¥18 / ¥30 / ¥68 / ¥128 / ¥328` 六个档位。全部档位参与首充双倍:用户历史上没有 `points_recharge` 流水时,本次购买到账泥点为基础泥点与等额赠送泥点之和;已有充值流水后只到账基础泥点。实际到账泥点写入交易流水,余额以 SpacetimeDB projection 为准。
|
泥点充值固定为 `¥6 / ¥18 / ¥30 / ¥68 / ¥128 / ¥328` 六个档位。全部档位参与首充双倍:用户历史上没有 `points_recharge` 流水时,本次购买到账泥点为基础泥点与等额赠送泥点之和;已有充值流水后只到账基础泥点。实际到账泥点写入交易流水,余额以 SpacetimeDB projection 为准。
|
||||||
|
|
||||||
|
充值中心返回的 `hasPointsRecharged` 是首充资格的展示与结算共同依据:当它为 `true` 时,后端下发的泥点套餐应只保留基础泥点、清空首充徽标与赠送文案;前端即使收到旧版本快照中残留的 `bonusPoints` / `badgeLabel`,也必须按 `hasPointsRecharged` 隐藏“首充双倍”和 `基础+赠送` 展示。这样可以避免第二次充值只到账基础泥点时,弹窗仍显示 `60+60` 等已失效权益。
|
||||||
|
|
||||||
### 2.2 会员卡套餐
|
### 2.2 会员卡套餐
|
||||||
|
|
||||||
| productId | 类型 | 天数 | 金额分 | 权益 |
|
| productId | 类型 | 天数 | 金额分 | 权益 |
|
||||||
@@ -144,9 +146,9 @@
|
|||||||
4. 点击套餐后调用下单接口,按钮进入处理中状态;小程序环境走 native 支付页拉起 `wx.requestPayment`,支付页返回后刷新 `profileDashboard`。
|
4. 点击套餐后调用下单接口,按钮进入处理中状态;小程序环境走 native 支付页拉起 `wx.requestPayment`,支付页返回后刷新 `profileDashboard`。
|
||||||
- 小程序 web-view 内的 H5 只负责加载微信 JS-SDK 并通过 `wx.miniProgram.navigateTo` 跳转到 `/pages/wechat-pay/index`;实际支付必须在小程序 native 页调用 `wx.requestPayment`,不要切换为 H5 支付产品。
|
- 小程序 web-view 内的 H5 只负责加载微信 JS-SDK 并通过 `wx.miniProgram.navigateTo` 跳转到 `/pages/wechat-pay/index`;实际支付必须在小程序 native 页调用 `wx.requestPayment`,不要切换为 H5 支付产品。
|
||||||
- native 支付页通过 `wx_pay_result=<requestId>:success|cancel|fail` 回填 web-view;H5 在 `hashchange`、`focus`、`pageshow` 和 `visibilitychange` 中都会尝试消费该结果,避免小程序返回 web-view 时没有触发单一事件导致状态不刷新。
|
- native 支付页通过 `wx_pay_result=<requestId>:success|cancel|fail` 回填 web-view;H5 在 `hashchange`、`focus`、`pageshow` 和 `visibilitychange` 中都会尝试消费该结果,避免小程序返回 web-view 时没有触发单一事件导致状态不刷新。
|
||||||
- `success` 只表示微信客户端支付流程返回成功,前端随后调用 `POST /api/profile/recharge/orders/{order_id}/wechat/confirm` 由服务端查单确认;只有通知或服务端查单确认为 `SUCCESS` 才入账。
|
- `success` 只表示微信客户端支付流程返回成功,前端随后调用 `POST /api/profile/recharge/orders/{order_id}/wechat/confirm` 由服务端查单确认;只有通知或服务端查单确认为 `SUCCESS` 才入账。
|
||||||
- 小程序返回后,前端会对确认接口做短轮询,覆盖微信通知/查单结果与 web-view 恢复之间的秒级时间差;只有确认响应里的订单状态变成 `paid` 后,才触发父级 `profileDashboard` 刷新,确保“我的”页泥点卡片读取到最新余额。
|
- 小程序返回后,前端会对确认接口做短轮询,覆盖微信通知/查单结果与 web-view 恢复之间的秒级时间差;只有确认响应里的订单状态变成 `paid` 后,才触发父级 `profileDashboard` 刷新,确保“我的”页泥点卡片读取到最新余额。
|
||||||
- `cancel` 和 `fail` 只复位按钮、刷新账户中心并通过全局支付结果模态展示,不调用入账逻辑。
|
- `cancel` 和 `fail` 只复位按钮、刷新账户中心并通过全局支付结果模态展示,不调用入账逻辑。
|
||||||
5. 支付结果使用页面级全局模态展示,不写回商品卡片或账户充值弹窗内部;充值弹窗只负责套餐选择、加载失败和下单失败。
|
5. 支付结果使用页面级全局模态展示,不写回商品卡片或账户充值弹窗内部;充值弹窗只负责套餐选择、加载失败和下单失败。
|
||||||
6. 弹窗内不写大段说明文案,只保留必要金额、泥点、会员权益和操作状态。
|
6. 弹窗内不写大段说明文案,只保留必要金额、泥点、会员权益和操作状态。
|
||||||
7. 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压。
|
7. 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压。
|
||||||
@@ -156,5 +158,6 @@
|
|||||||
1. 普通用户打开弹窗能看到泥点与会员套餐。
|
1. 普通用户打开弹窗能看到泥点与会员套餐。
|
||||||
2. 泥点购买后余额增加,流水来源为 `points_recharge`。
|
2. 泥点购买后余额增加,流水来源为 `points_recharge`。
|
||||||
3. 首充赠送只在首次泥点充值时生效。
|
3. 首充赠送只在首次泥点充值时生效。
|
||||||
4. 会员购买后会员状态与到期时间立即更新。
|
4. 已产生 `points_recharge` 流水后,再打开充值弹窗不应展示“首充双倍”徽标或 `60+60` 等赠送泥点组合。
|
||||||
5. 移动端弹窗单列可滚动,桌面端接近参考图卡片网格。
|
5. 会员购买后会员状态与到期时间立即更新。
|
||||||
|
6. 移动端弹窗单列可滚动,桌面端接近参考图卡片网格。
|
||||||
|
|||||||
@@ -77,6 +77,21 @@ pub fn runtime_profile_recharge_point_products() -> Vec<RuntimeProfileRechargePr
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 中文注释:充值中心展示当前账号本次实际可生效的首充赠送状态。
|
||||||
|
pub fn resolve_runtime_profile_recharge_point_products(
|
||||||
|
has_points_recharged: bool,
|
||||||
|
) -> Vec<RuntimeProfileRechargeProductSnapshot> {
|
||||||
|
let mut products = runtime_profile_recharge_point_products();
|
||||||
|
if has_points_recharged {
|
||||||
|
for product in &mut products {
|
||||||
|
product.bonus_points = 0;
|
||||||
|
product.badge_label.clear();
|
||||||
|
product.description = product.title.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
products
|
||||||
|
}
|
||||||
|
|
||||||
pub fn runtime_profile_recharge_membership_products() -> Vec<RuntimeProfileRechargeProductSnapshot>
|
pub fn runtime_profile_recharge_membership_products() -> Vec<RuntimeProfileRechargeProductSnapshot>
|
||||||
{
|
{
|
||||||
vec![
|
vec![
|
||||||
@@ -706,6 +721,22 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recharge_point_products_resolve_effective_first_bonus_display() {
|
||||||
|
let first_recharge_products = resolve_runtime_profile_recharge_point_products(false);
|
||||||
|
assert_eq!(first_recharge_products[0].bonus_points, 60);
|
||||||
|
assert_eq!(first_recharge_products[0].badge_label, "首充双倍");
|
||||||
|
assert_eq!(first_recharge_products[0].description, "首充送60泥点");
|
||||||
|
|
||||||
|
let repeated_recharge_products = resolve_runtime_profile_recharge_point_products(true);
|
||||||
|
assert_eq!(repeated_recharge_products[0].bonus_points, 0);
|
||||||
|
assert_eq!(repeated_recharge_products[0].badge_label, "");
|
||||||
|
assert_eq!(repeated_recharge_products[0].description, "60泥点");
|
||||||
|
assert_eq!(repeated_recharge_products[5].bonus_points, 0);
|
||||||
|
assert_eq!(repeated_recharge_products[5].badge_label, "");
|
||||||
|
assert_eq!(repeated_recharge_products[5].description, "3280泥点");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_recharge_order_input_rejects_unknown_product() {
|
fn build_recharge_order_input_rejects_unknown_product() {
|
||||||
let error = build_runtime_profile_recharge_order_create_input(
|
let error = build_runtime_profile_recharge_order_create_input(
|
||||||
|
|||||||
@@ -2915,16 +2915,18 @@ fn build_profile_recharge_center_snapshot(
|
|||||||
.map(|row| row.wallet_balance)
|
.map(|row| row.wallet_balance)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let has_points_recharged = has_profile_points_recharged(ctx, user_id);
|
||||||
|
|
||||||
RuntimeProfileRechargeCenterSnapshot {
|
RuntimeProfileRechargeCenterSnapshot {
|
||||||
user_id: user_id.to_string(),
|
user_id: user_id.to_string(),
|
||||||
wallet_balance,
|
wallet_balance,
|
||||||
membership: build_profile_membership_snapshot(ctx, user_id),
|
membership: build_profile_membership_snapshot(ctx, user_id),
|
||||||
point_products: runtime_profile_recharge_point_products(),
|
point_products: resolve_runtime_profile_recharge_point_products(has_points_recharged),
|
||||||
membership_products: runtime_profile_recharge_membership_products(),
|
membership_products: runtime_profile_recharge_membership_products(),
|
||||||
benefits: runtime_profile_membership_benefits(),
|
benefits: runtime_profile_membership_benefits(),
|
||||||
latest_order: latest_profile_recharge_order(ctx, user_id)
|
latest_order: latest_profile_recharge_order(ctx, user_id)
|
||||||
.map(|row| build_profile_recharge_order_snapshot_from_row(&row)),
|
.map(|row| build_profile_recharge_order_snapshot_from_row(&row)),
|
||||||
has_points_recharged: has_profile_points_recharged(ctx, user_id),
|
has_points_recharged,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -675,6 +675,10 @@ function renderProfileView(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openRechargeModal(user: ReturnType<typeof userEvent.setup>) {
|
||||||
|
await user.click(screen.getByRole('button', { name: /充值\s*泥点\/会员/u }));
|
||||||
|
}
|
||||||
|
|
||||||
function renderLoggedOutHomeView(
|
function renderLoggedOutHomeView(
|
||||||
openLoginModal = vi.fn(),
|
openLoginModal = vi.fn(),
|
||||||
overrides: Partial<
|
overrides: Partial<
|
||||||
@@ -924,7 +928,9 @@ afterEach(() => {
|
|||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
window.wx = undefined;
|
window.wx = undefined;
|
||||||
document
|
document
|
||||||
.querySelectorAll('script[src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"]')
|
.querySelectorAll(
|
||||||
|
'script[src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"]',
|
||||||
|
)
|
||||||
.forEach((script) => script.remove());
|
.forEach((script) => script.remove());
|
||||||
mockGetRpgProfileReferralInviteCenter.mockResolvedValue(
|
mockGetRpgProfileReferralInviteCenter.mockResolvedValue(
|
||||||
mockBuildReferralCenter(),
|
mockBuildReferralCenter(),
|
||||||
@@ -1016,10 +1022,7 @@ test('profile recharge modal buys points through mock channel outside mini progr
|
|||||||
const onRechargeSuccess = vi.fn();
|
const onRechargeSuccess = vi.fn();
|
||||||
|
|
||||||
renderProfileView(onRechargeSuccess);
|
renderProfileView(onRechargeSuccess);
|
||||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
await openRechargeModal(user);
|
||||||
await user.click(
|
|
||||||
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(await screen.findByText('账户充值')).toBeTruthy();
|
expect(await screen.findByText('账户充值')).toBeTruthy();
|
||||||
expect(mockGetRpgProfileRechargeCenter).toHaveBeenCalledTimes(1);
|
expect(mockGetRpgProfileRechargeCenter).toHaveBeenCalledTimes(1);
|
||||||
@@ -1031,13 +1034,52 @@ test('profile recharge modal buys points through mock channel outside mini progr
|
|||||||
'mock',
|
'mock',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(
|
expect(await screen.findByRole('dialog', { name: '支付成功' })).toBeTruthy();
|
||||||
await screen.findByRole('dialog', { name: '支付成功' }),
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(screen.getByText('已到账,账户状态已刷新。')).toBeTruthy();
|
expect(screen.getByText('已到账,账户状态已刷新。')).toBeTruthy();
|
||||||
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('profile recharge modal hides first bonus display after points recharge', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
mockGetRpgProfileRechargeCenter.mockResolvedValueOnce({
|
||||||
|
walletBalance: 60,
|
||||||
|
membership: {
|
||||||
|
status: 'normal',
|
||||||
|
tier: 'normal',
|
||||||
|
startedAt: null,
|
||||||
|
expiresAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
},
|
||||||
|
pointProducts: [
|
||||||
|
{
|
||||||
|
productId: 'points_60',
|
||||||
|
title: '60泥点',
|
||||||
|
priceCents: 600,
|
||||||
|
kind: 'points',
|
||||||
|
pointsAmount: 60,
|
||||||
|
bonusPoints: 60,
|
||||||
|
durationDays: 0,
|
||||||
|
badgeLabel: '首充双倍',
|
||||||
|
description: '首充送60泥点',
|
||||||
|
tier: 'normal',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
membershipProducts: [],
|
||||||
|
benefits: [],
|
||||||
|
latestOrder: null,
|
||||||
|
hasPointsRecharged: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
renderProfileView();
|
||||||
|
await openRechargeModal(user);
|
||||||
|
|
||||||
|
const rechargeDialog = await screen.findByText('账户充值');
|
||||||
|
expect(rechargeDialog).toBeTruthy();
|
||||||
|
expect(screen.getByRole('button', { name: /60泥点/u })).toBeTruthy();
|
||||||
|
expect(screen.queryByText('首充双倍')).toBeNull();
|
||||||
|
expect(screen.queryByText('60+60泥点')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
test('profile recharge modal posts requestPayment params in mini program web-view', async () => {
|
test('profile recharge modal posts requestPayment params in mini program web-view', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onRechargeSuccess = vi.fn();
|
const onRechargeSuccess = vi.fn();
|
||||||
@@ -1090,10 +1132,7 @@ test('profile recharge modal posts requestPayment params in mini program web-vie
|
|||||||
});
|
});
|
||||||
|
|
||||||
renderProfileView(onRechargeSuccess);
|
renderProfileView(onRechargeSuccess);
|
||||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
await openRechargeModal(user);
|
||||||
await user.click(
|
|
||||||
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
|
||||||
);
|
|
||||||
await user.click(await screen.findByRole('button', { name: /60泥点/u }));
|
await user.click(await screen.findByRole('button', { name: /60泥点/u }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -1118,9 +1157,7 @@ test('profile recharge modal posts requestPayment params in mini program web-vie
|
|||||||
});
|
});
|
||||||
expect(navigateUrl).toContain('order-wechat-1');
|
expect(navigateUrl).toContain('order-wechat-1');
|
||||||
expect(decodeURIComponent(navigateUrl)).toContain('prepay_id=wx-prepay');
|
expect(decodeURIComponent(navigateUrl)).toContain('prepay_id=wx-prepay');
|
||||||
expect(
|
expect(await screen.findByRole('dialog', { name: '支付成功' })).toBeTruthy();
|
||||||
await screen.findByRole('dialog', { name: '支付成功' }),
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(screen.getByText('已到账,账户状态已刷新。')).toBeTruthy();
|
expect(screen.getByText('已到账,账户状态已刷新。')).toBeTruthy();
|
||||||
expect(mockConfirmWechatRpgProfileRechargeOrder).toHaveBeenCalledWith(
|
expect(mockConfirmWechatRpgProfileRechargeOrder).toHaveBeenCalledWith(
|
||||||
'order-wechat-1',
|
'order-wechat-1',
|
||||||
@@ -1243,10 +1280,7 @@ test('profile recharge modal waits for paid confirmation before refreshing dashb
|
|||||||
});
|
});
|
||||||
|
|
||||||
renderProfileView(onRechargeSuccess);
|
renderProfileView(onRechargeSuccess);
|
||||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
await openRechargeModal(user);
|
||||||
await user.click(
|
|
||||||
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
|
||||||
);
|
|
||||||
await user.click(await screen.findByRole('button', { name: /60泥点/u }));
|
await user.click(await screen.findByRole('button', { name: /60泥点/u }));
|
||||||
|
|
||||||
const navigateUrl = navigateTo.mock.calls[0]?.[0].url ?? '';
|
const navigateUrl = navigateTo.mock.calls[0]?.[0].url ?? '';
|
||||||
@@ -1266,9 +1300,7 @@ test('profile recharge modal waits for paid confirmation before refreshing dashb
|
|||||||
expect(mockConfirmWechatRpgProfileRechargeOrder).toHaveBeenCalledTimes(2);
|
expect(mockConfirmWechatRpgProfileRechargeOrder).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(await screen.findByRole('dialog', { name: '支付成功' })).toBeTruthy();
|
||||||
await screen.findByRole('dialog', { name: '支付成功' }),
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1319,10 +1351,7 @@ test('profile recharge modal loads wechat js sdk before mini program payment bri
|
|||||||
});
|
});
|
||||||
|
|
||||||
renderProfileView();
|
renderProfileView();
|
||||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
await openRechargeModal(user);
|
||||||
await user.click(
|
|
||||||
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
|
||||||
);
|
|
||||||
await user.click(await screen.findByRole('button', { name: /60泥点/u }));
|
await user.click(await screen.findByRole('button', { name: /60泥点/u }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -1354,9 +1383,7 @@ test('profile recharge modal loads wechat js sdk before mini program payment bri
|
|||||||
window.location.hash = `wx_pay_result=${requestId}:success`;
|
window.location.hash = `wx_pay_result=${requestId}:success`;
|
||||||
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
||||||
});
|
});
|
||||||
expect(
|
expect(await screen.findByRole('dialog', { name: '支付成功' })).toBeTruthy();
|
||||||
await screen.findByRole('dialog', { name: '支付成功' }),
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('profile recharge modal releases submitting state after cancelled wechat pay result', async () => {
|
test('profile recharge modal releases submitting state after cancelled wechat pay result', async () => {
|
||||||
@@ -1410,10 +1437,7 @@ test('profile recharge modal releases submitting state after cancelled wechat pa
|
|||||||
});
|
});
|
||||||
|
|
||||||
renderProfileView();
|
renderProfileView();
|
||||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
await openRechargeModal(user);
|
||||||
await user.click(
|
|
||||||
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
|
||||||
);
|
|
||||||
const buyButton = await screen.findByRole('button', { name: /60泥点/u });
|
const buyButton = await screen.findByRole('button', { name: /60泥点/u });
|
||||||
await user.click(buyButton);
|
await user.click(buyButton);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ArrowRight,
|
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
ArrowRight,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
Camera,
|
Camera,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
Heart,
|
Heart,
|
||||||
LogIn,
|
LogIn,
|
||||||
MessageCircle,
|
MessageCircle,
|
||||||
Pencil,
|
|
||||||
Palette,
|
Palette,
|
||||||
|
Pencil,
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -47,22 +47,22 @@ import communityQqQrImage from '../../../media/social-media-group/qq.png';
|
|||||||
import communityWechatQrImage from '../../../media/social-media-group/wechat.png';
|
import communityWechatQrImage from '../../../media/social-media-group/wechat.png';
|
||||||
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
|
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
|
||||||
import type {
|
import type {
|
||||||
|
ConfirmWechatProfileRechargeOrderResponse,
|
||||||
CustomWorldLibraryEntry,
|
CustomWorldLibraryEntry,
|
||||||
PlatformBrowseHistoryEntry,
|
PlatformBrowseHistoryEntry,
|
||||||
ConfirmWechatProfileRechargeOrderResponse,
|
|
||||||
ProfileDashboardCardKey,
|
ProfileDashboardCardKey,
|
||||||
ProfileDashboardSummary,
|
ProfileDashboardSummary,
|
||||||
ProfilePlayedWorkSummary,
|
ProfilePlayedWorkSummary,
|
||||||
ProfilePlayStatsResponse,
|
ProfilePlayStatsResponse,
|
||||||
ProfileReferralInviteCenterResponse,
|
|
||||||
ProfileRechargeCenterResponse,
|
ProfileRechargeCenterResponse,
|
||||||
ProfileRechargeProduct,
|
ProfileRechargeProduct,
|
||||||
WechatMiniProgramPayParams,
|
ProfileReferralInviteCenterResponse,
|
||||||
ProfileSaveArchiveSummary,
|
ProfileSaveArchiveSummary,
|
||||||
ProfileTaskCenterResponse,
|
ProfileTaskCenterResponse,
|
||||||
ProfileTaskItem,
|
ProfileTaskItem,
|
||||||
ProfileWalletLedgerResponse,
|
ProfileWalletLedgerResponse,
|
||||||
RedeemProfileRewardCodeResponse,
|
RedeemProfileRewardCodeResponse,
|
||||||
|
WechatMiniProgramPayParams,
|
||||||
} from '../../../packages/shared/src/contracts/runtime';
|
} from '../../../packages/shared/src/contracts/runtime';
|
||||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||||
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
|
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
|
||||||
@@ -77,8 +77,8 @@ import {
|
|||||||
claimRpgProfileTaskReward,
|
claimRpgProfileTaskReward,
|
||||||
confirmWechatRpgProfileRechargeOrder,
|
confirmWechatRpgProfileRechargeOrder,
|
||||||
createRpgProfileRechargeOrder,
|
createRpgProfileRechargeOrder,
|
||||||
getRpgProfileReferralInviteCenter,
|
|
||||||
getRpgProfileRechargeCenter,
|
getRpgProfileRechargeCenter,
|
||||||
|
getRpgProfileReferralInviteCenter,
|
||||||
getRpgProfileTasks,
|
getRpgProfileTasks,
|
||||||
getRpgProfileWalletLedger,
|
getRpgProfileWalletLedger,
|
||||||
redeemRpgProfileReferralInviteCode,
|
redeemRpgProfileReferralInviteCode,
|
||||||
@@ -2498,17 +2498,23 @@ async function confirmWechatRechargeOrderUntilSettled(
|
|||||||
|
|
||||||
function RechargeProductCard({
|
function RechargeProductCard({
|
||||||
product,
|
product,
|
||||||
|
hasPointsRecharged,
|
||||||
submittingProductId,
|
submittingProductId,
|
||||||
onBuy,
|
onBuy,
|
||||||
}: {
|
}: {
|
||||||
product: ProfileRechargeProduct;
|
product: ProfileRechargeProduct;
|
||||||
|
hasPointsRecharged: boolean;
|
||||||
submittingProductId: string | null;
|
submittingProductId: string | null;
|
||||||
onBuy: (product: ProfileRechargeProduct) => void;
|
onBuy: (product: ProfileRechargeProduct) => void;
|
||||||
}) {
|
}) {
|
||||||
const submitting = submittingProductId === product.productId;
|
const submitting = submittingProductId === product.productId;
|
||||||
|
const effectiveBonusPoints =
|
||||||
|
product.kind === 'points' && hasPointsRecharged ? 0 : product.bonusPoints;
|
||||||
|
const badgeLabel =
|
||||||
|
product.kind === 'points' && hasPointsRecharged ? '' : product.badgeLabel;
|
||||||
const value =
|
const value =
|
||||||
product.kind === 'points'
|
product.kind === 'points'
|
||||||
? `${product.pointsAmount}${product.bonusPoints > 0 ? `+${product.bonusPoints}` : ''}泥点`
|
? `${product.pointsAmount}${effectiveBonusPoints > 0 ? `+${effectiveBonusPoints}` : ''}泥点`
|
||||||
: `${product.durationDays}天`;
|
: `${product.durationDays}天`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -2518,9 +2524,9 @@ function RechargeProductCard({
|
|||||||
disabled={Boolean(submittingProductId)}
|
disabled={Boolean(submittingProductId)}
|
||||||
className="platform-subpanel platform-interactive-card relative min-h-[7.25rem] rounded-[1.15rem] px-3.5 py-3.5 text-left disabled:cursor-not-allowed disabled:opacity-60"
|
className="platform-subpanel platform-interactive-card relative min-h-[7.25rem] rounded-[1.15rem] px-3.5 py-3.5 text-left disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
>
|
>
|
||||||
{product.badgeLabel ? (
|
{badgeLabel ? (
|
||||||
<span className="platform-pill platform-pill--warm absolute right-3 top-3 max-w-[7rem] truncate px-2 py-0.5 text-[10px]">
|
<span className="platform-pill platform-pill--warm absolute right-3 top-3 max-w-[7rem] truncate px-2 py-0.5 text-[10px]">
|
||||||
{product.badgeLabel}
|
{badgeLabel}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="pr-20 text-sm font-black text-[var(--platform-text-strong)]">
|
<div className="pr-20 text-sm font-black text-[var(--platform-text-strong)]">
|
||||||
@@ -2640,6 +2646,7 @@ function ProfileRechargeModal({
|
|||||||
<RechargeProductCard
|
<RechargeProductCard
|
||||||
key={product.productId}
|
key={product.productId}
|
||||||
product={product}
|
product={product}
|
||||||
|
hasPointsRecharged={center?.hasPointsRecharged === true}
|
||||||
submittingProductId={submittingProductId}
|
submittingProductId={submittingProductId}
|
||||||
onBuy={onBuy}
|
onBuy={onBuy}
|
||||||
/>
|
/>
|
||||||
@@ -3928,7 +3935,7 @@ export function RpgEntryHomeView({
|
|||||||
setIsWalletLedgerOpen(true);
|
setIsWalletLedgerOpen(true);
|
||||||
loadWalletLedger();
|
loadWalletLedger();
|
||||||
};
|
};
|
||||||
const loadRechargeCenter = () => {
|
const loadRechargeCenter = useCallback(() => {
|
||||||
setRechargeError(null);
|
setRechargeError(null);
|
||||||
setIsLoadingRechargeCenter(true);
|
setIsLoadingRechargeCenter(true);
|
||||||
void getRpgProfileRechargeCenter()
|
void getRpgProfileRechargeCenter()
|
||||||
@@ -3940,15 +3947,12 @@ export function RpgEntryHomeView({
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoadingRechargeCenter(false));
|
.finally(() => setIsLoadingRechargeCenter(false));
|
||||||
};
|
}, []);
|
||||||
const refreshRechargeState = useCallback(
|
const refreshRechargeState = useCallback(() => {
|
||||||
() => {
|
loadRechargeCenter();
|
||||||
loadRechargeCenter();
|
setSubmittingRechargeProductId(null);
|
||||||
setSubmittingRechargeProductId(null);
|
pendingWechatRechargeOrderIdRef.current = null;
|
||||||
pendingWechatRechargeOrderIdRef.current = null;
|
}, [loadRechargeCenter]);
|
||||||
},
|
|
||||||
[loadRechargeCenter],
|
|
||||||
);
|
|
||||||
const handleWechatPayResult = useCallback(() => {
|
const handleWechatPayResult = useCallback(() => {
|
||||||
const payResult = readWechatPayResultFromHash();
|
const payResult = readWechatPayResultFromHash();
|
||||||
if (!payResult) {
|
if (!payResult) {
|
||||||
|
|||||||
Reference in New Issue
Block a user