AWS Amplify Flutter: how to create platform endpoint?

595 views Asked by At

Short version of my question: I have a Flutter mobile app that uses AWS Amplify to handle user sign-up and log-in. Now I need to add push notification capability (with AWS SNS, APN etc.) to the app, and in particular I need to let the app itself to create the platform endpoint. So now I have to call the CreatePlatformEndpoint function in an Amplify Flutter mobile app, and I don't know how to do it correctly. Any idea?

Here's what I've done so far: Firstly, I configured AWS Cognito and added Amplify to my app so that the user can sign up and log in. The relevant code looks something like the following:

// Sign Up
var userAttributes = {
  CognitoUserAttributeKey.email: email,
};
await Amplify.Auth.signUp(
  username: email,
  password: password,
  options: CognitoSignUpOptions(
    userAttributes: userAttributes,
  ),
);

// Log In
var res = await Amplify.Auth.signIn(
  username: email,
  password: password,
);
if (res.isSignedIn) {
  ....
} else {
  ....
}

This part is successful and the code works all right.

Secondly, I configured APN, AWS SNS, and the iOS module of the app, then modified AppDelegate to something like the following:

override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    registerForNotifications()
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

private func registerForNotifications() {
    UNUserNotificationCenter.current().delegate = self
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, _ in
        guard granted else { return }
        self?.getNotificationSettings()
    }
}

private func getNotificationSettings() {
    UNUserNotificationCenter.current().getNotificationSettings { settings in
        guard settings.authorizationStatus == .authorized else { return }
        DispatchQueue.main.async {
            UIApplication.shared.registerForRemoteNotifications()
        }
    }
}

override func application(
    _ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken tokenData: Data
) {
    let token = tokenData.map { String(format: "%02.2hhx", $0) }.joined()
    print("token: \(token)")
}

override func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
    completionHandler([.badge, .sound, .alert])
}

This part is also successful. The iPhone can obtain and print the device token, and when I manually create the endpoint in AWS Management Console and send a notification from there, the iPhone can receive and show the notification.

Finally, I tried to let the app automatically create the endpoint in SNS when it got the device token. I referenced this article, and managed to modify the didRegisterForRemoteNotificationsWithDeviceToken method to the following:

override func application(
    _ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken tokenData: Data
) {
    let token = tokenData.map { String(format: "%02.2hhx", $0) }.joined()
    print("token: \(token)")
    let credentialsProvider = AWSCognitoCredentialsProvider(regionType: ......, identityPoolId: ".........")
    let configuration = AWSServiceConfiguration(region: ......, credentialsProvider: credentialsProvider)
    AWSServiceManager.default().defaultServiceConfiguration = configuration
    let appArn = "arn:aws:sns:........"
    let platformEndpointRequest = AWSSNSCreatePlatformEndpointInput()!
    platformEndpointRequest.customUserData = "........."
    platformEndpointRequest.token = token
    platformEndpointRequest.platformApplicationArn = appArn
    let sns = AWSSNS.default()
    sns.createPlatformEndpoint(platformEndpointRequest) { response, error in
        ....
    }
}

This part is only partially successful. The endpoint is indeed created in SNS, and I can verify this in AWS Management Console. But there are serious problems:

  • the user is logged out after the above code is executed
  • when the user tries to log back in, the app crashes with an "unexpectedly found nil" exception

Here's the crash log:

AWSMobileClient/AWSMobileClient+SignIn.swift:66: Fatal error: Unexpectedly found nil while unwrapping an Optional value
AWSMobileClient/AWSMobileClient+SignIn.swift:66: Fatal error: Unexpectedly found nil while unwrapping an Optional value
* thread #4, queue = 'NSOperationQueue 0x107d22800 (QOS: UNSPECIFIED)', stop reason = Fatal error: Unexpectedly found nil while unwrapping an Optional value
    frame #0: 0x000000019b861060 libswiftCore.dylib`_swift_runtime_on_report
libswiftCore.dylib`_swift_runtime_on_report:
->  0x19b861060 : ret    
libswiftCore.dylib`_swift_reportToDebugger:
    0x19b861064 : b      0x19b861060               ; _swift_runtime_on_report
libswiftCore.dylib`_swift_shouldReportFatalErrorsToDebugger:
    0x19b861068 : adrp   x8, 324077
    0x19b86106c : ldrb   w0, [x8, #0x611]
Target 0: (Runner) stopped.

I think maybe the defaultServiceConfiguration = configuration thing conflicts with Amplify, so I try removing that part and modify the code to:

override func application(
    _ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken tokenData: Data
) {
    let token = tokenData.map { String(format: "%02.2hhx", $0) }.joined()
    print("token: \(token)")
    let appArn = "arn:aws:sns:........"
    let platformEndpointRequest = AWSSNSCreatePlatformEndpointInput()!
    platformEndpointRequest.customUserData = "........."
    platformEndpointRequest.token = token
    platformEndpointRequest.platformApplicationArn = appArn
    let sns = AWSSNS.default()
    sns.createPlatformEndpoint(platformEndpointRequest) { response, error in
        ....
    }
}

but then the code crashes at the createPlatformEndpoint call, and here's the crash log:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The service configuration is `nil`. You need to configure `awsconfiguration.json`, `Info.plist` or set `defaultServiceConfiguration` before using this method.'
*** First throw call stack:
(0x197c1f128 0x1ab932480 0x102d7fcf4 0x197857298 0x1977faf90 0x102d7fb5c 0x102c875a0 0x102c86e4c 0x102c872c0 0x19a4d8c34 0x1978562b0 0x197857298 0x197805ce4 0x197b9e170 0x197b985d0 0x197b976a8 0x1ae247570 0x19a4b5370 0x19a4ba8ec 0x102c88400 0x197876140)
libc++abi: terminating with uncaught exception of type NSException
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
    frame #0: 0x00000001c38b47b0 libsystem_kernel.dylib`__pthread_kill + 8
libsystem_kernel.dylib`__pthread_kill:
->  0x1c38b47b0 :  b.lo   0x1c38b47cc               ; 
    0x1c38b47b4 : stp    x29, x30, [sp, #-0x10]!
    0x1c38b47b8 : mov    x29, sp
    0x1c38b47bc : bl     0x1c38911fc               ; cerror_nocancel
Target 0: (Runner) stopped.

So what should I do? What's the correct way to create an endpoint in an Amplify Flutter app?

1

There are 1 answers

0
ycsun On

OK, I'll answer my own question. I asked the Amplify-Flutter people and learned that there was currently no way to do what I want. I was advised to create a feature request ticket in their repository. I decided to turn to another approach, i.e. creating a Lambda that did the job on the backend, and it worked fine.