You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular中PayPal按钮约50%概率加载失败求助

PayPal按钮在Angular页面中随机加载失败的问题

问题现象

  • 按钮约50%概率无法加载,连续刷新30次出现9次成功→11次失败→10次成功的交替规律
  • 测试环境与生产环境表现一致

失败时错误信息

TypeError: Cannot read properties of undefined (reading 'nativeElement')
at deposittest.component.ts:218:36
at _ZoneDelegate.invoke (zone.js:368:26)
at Object.onInvoke (core.mjs:26321:33)
at _ZoneDelegate.invoke (zone.js:367:52)
at Zone.run (zone.js:129:43)
at zone.js:1257:36
at _ZoneDelegate.invokeTask (zone.js:402:31)
at core.mjs:25998:55
at AsyncStackTaggingZoneSpec.onInvokeTask (core.mjs:25998:36)
at _ZoneDelegate.invokeTask (zone.js:401:60)

相关代码

组件TS文件

import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { loadScript, PayPalNamespace } from "@paypal/paypal-js";

@Component({
  selector: 'app-deposittest',
  templateUrl: './deposittest.component.html',
  styleUrls: ['./deposittest.component.css']
})
export class DeposittestComponent implements OnInit {
  depositState$: Observable<State<CustomHttpResponse<User>>>;
  private dataSubject = new BehaviorSubject<CustomHttpResponse<User>>(null);
  private isLoadingSubject = new BehaviorSubject<boolean>(false);
  isLoading$ = this.isLoadingSubject.asObservable();
 
  DataState = DataState;

  private isLoggedInSubject = new BehaviorSubject<boolean>(false);
  isLoggedIn$ = this.isLoggedInSubject.asObservable();

  PAYPAL_CLIENT_ID: string = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

  myAmount: any

  @ViewChild('paypalRef', { static: false }) private paypalRef!: ElementRef;

  constructor(private notificationService: NotificationService, private router: Router, private userService: UserService) {
  }
  ngOnInit(): void {
    this.depositState$ = this.userService.depositInit$()
      .pipe(
        map(response => {
          console.log(response);
          this.dataSubject.next(response);
          return {
            dataState: DataState.LOADED, appData: response
          };
        }),
        startWith({ dataState: DataState.LOADING }),
        catchError((error: string) => {
          return of({ dataState: DataState.ERROR, error })
        })
      )
    this.renderPaypalButton();
  }

  renderPaypalButton() {
    let orderRef: string | null = null;
    loadScript({
      "clientId": this.PAYPAL_CLIENT_ID,
      currency: 'USD',
      disableFunding: 'paylater,credit,card,venmo',
    })
      .then((loadedPaypal) => {
        if (loadedPaypal) {
          const paypal: PayPalNamespace = loadedPaypal;
          paypal.Buttons({
            style: {
              layout: 'vertical',
              color: 'blue',
              shape: 'rect',
              label: 'paypal',
            },
            createOrder: (data, actions) => {
              const postData = {
                amount: this.myAmount,
                state: this.state
              };
              return fetch(AppSettings.API_ENDPOINT + '/user/create_paypal_transaction', {
                method: "post",
                headers: {
                  'Authorization': 'Bearer ' + localStorage.getItem(Key.TOKEN),
                  'Accept': 'application/json, text/plain, */*',
                  'Content-Type': 'application/json'
                },
                body: JSON.stringify(postData)
              })
                .then((response) => response.json())
                .then((response) => {
                  return response.data.order.id;
                });
            },
            onApprove: (data, actions) => {
              return fetch(AppSettings.API_ENDPOINT + `/user/paypal/capture_order/${data.orderID}`, {
                method: "post",
                headers: {
                  'Authorization': 'Bearer ' + localStorage.getItem(Key.TOKEN),
                  'Accept': 'application/json, text/plain, */*',
                  'Content-Type': 'application/json',
                },
              })
                .then((response) => response.json())
                .then((response) => {
                  this.paypalRedirect(response.data.order.status, orderRef);
                });
            },
            onCancel: () => {
              // 
            },
            onError: (err) => {
              this.notificationService.onError('Error processing deposit.');
              this.paypalRedirect('ERROR', orderRef);
            }
          }).render(this.paypalRef.nativeElement);
        } else {
          console.error("");
        }
      })
      .catch((error) => {
        console.error("", error);
      });
  }
  paypalRedirect(status: string, orderRef: string | null) {
    switch (status) {
      case 'COMPLETED':
        if (orderRef) {
         this.notificationService.onDefault('Payment Successful.');
          this.router.navigate(['/paypal/result/success/']);
        } else {
          console.error("Missing success redirect");
        }
        break;
      case 'ERROR':
        if (orderRef) {
          this.notificationService.onError('Payment could not be completed.');
          this.router.navigate(['/']);
        } else {
          console.error("Missing failure redircte");
        }
        break;
      default:
        console.error("redirection.");
        break;
    }
  }
}

组件HTML文件

<ng-container *ngIf="(depositState$ | async) as state" [ngSwitch]="state.dataState">
    <ng-container *ngSwitchCase="DataState.LOADED">
        <app-navbar [user]="state?.appData?.data?.user"></app-navbar>
        <section>
            <div id="payment" class="mt-3">
                <div class="col-md-12">
                    <div class="form-group">
                        <div style="font-weight: bold;">Name on file: {{ state?.appData?.data?.user.firstName+'
                            '+state?.appData?.data?.user.lastName}} </div>
                        <div class="mt-2" style="font-style: italic;font-size: 1rem;">Your name on file must match the
                            name associated with your form of payment.</div>
                    </div>
                </div>
                <div id="input" class="form-group mt-3">
                    <label style="font-size: .8rem;" class="me-2">Enter Amount: $10 - $999</label>
                    <input type="text" (keypress)="decimalFilter($event)" placeholder="Enter amount here" ngModel
                        name="myAmount" (ngModelChange)="myAmount=$event" value="{{myAmount }}"
                        class="form-control mb-2">
                </div>
                <div #paypalRef></div>
            </div>
        </section>
    </ng-container>
    <ng-container *ngSwitchCase="DataState.LOADING">
        <div>Loading...</div>
    </ng-container>
    <ng-container *ngSwitchCase="DataState.ERROR">
        <div>{{ state.error }}</div>
    </ng-container>
    <app-authfooter></app-authfooter>
</ng-container>

核心原因

错误指向this.paypalRef.nativeElement未定义,本质是PayPal脚本加载完成时,#paypalRef元素还未被Angular渲染。因为#paypalRef仅在DataState.LOADED状态下才会被渲染,而当前在ngOnInit中直接调用renderPaypalButton(),脚本加载的异步过程与组件数据加载的异步过程未同步,谁先完成完全随机,导致一半概率元素未渲染就被访问。

修复方案

方案1:数据加载完成后再渲染按钮(优先推荐)

将renderPaypalButton()的调用移到depositState$数据流的LOADED状态回调中,确保元素已渲染:

ngOnInit(): void {
  this.depositState$ = this.userService.depositInit$()
    .pipe(
      map(response => {
        console.log(response);
        this.dataSubject.next(response);
        // 数据加载完成,此时#paypalRef已渲染,调用按钮渲染
        this.renderPaypalButton();
        return {
          dataState: DataState.LOADED, appData: response
        };
      }),
      startWith({ dataState: DataState.LOADING }),
      catchError((error: string) => {
        return of({ dataState: DataState.ERROR, error })
      })
    )
  // 移除原ngOnInit中的this.renderPaypalButton();
}

方案2:添加元素存在性检查(备选)

如果方案1仍有偶发问题,可在renderPaypalButton中先检查元素是否存在,不存在则延迟重试:

renderPaypalButton() {
  // 先检查元素是否存在,不存在则延迟重试
  if (!this.paypalRef?.nativeElement) {
    setTimeout(() => this.renderPaypalButton(), 100);
    return;
  }
  // 原有的loadScript逻辑...
}

额外优化点

  • 在renderPaypalButton的异常分支添加具体错误信息,方便排查:
    .catch((error) => {
      console.error("PayPal脚本加载失败:", error);
    });
    // ...
    } else {
      console.error("PayPal脚本未成功加载");
    }
    
  • 添加标志位isPaypalRendered,防止多次调用renderPaypalButton导致重复创建按钮。

内容的提问来源于stack exchange,提问作者T M

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 05:24:56