I'm trying to allow achievements in an EOS game, but login gives me a "user_not_found" error

I got it working. Had to create a user. Here’s the working code.

.hpp:

// Copyright 2024 David Simoes - All rights reserved.

#pragma once

#include "CoreMinimal.h"

#include <eos_init.h>
#include <eos_common.h>
#include <eos_sdk.h>
#include <eos_achievements.h>
	
/**
 *
 */
class CHRONO_API EosSingleton
{
public:
	// Delete copy constructor and assignment operator to prevent copies
	EosSingleton(const EosSingleton &) = delete;
	EosSingleton &operator=(const EosSingleton &) = delete;

	// Static method to access the singleton instance
	static EosSingleton &getInstance()
	{
		static EosSingleton instance; // Guaranteed to be destroyed, instantiated on first use
		return instance;
	}

	void tick();

	void UnlockAchievements(const char **AchievementIds, uint32_t AchievementsCount);

private:
	EosSingleton();
	~EosSingleton();

	void InitSdk();
	void CreatePlatform();
	void AuthLogin(char *token_arg);
	void ConnectInterface(EOS_EpicAccountId account_id);
	void ConnectLogin(const char *token_arg);
	void CreateUser(const EOS_ContinuanceToken &ContinuanceToken);

	EOS_ProductUserId LocalProductUserId = nullptr; // not the same as EOS_EpicAccountId
	EOS_HAuth AuthHandle = nullptr;
	EOS_HPlatform PlatformHandle = nullptr;
	EOS_HAchievements AchievementsHandle = nullptr;
	EOS_HConnect ConnectionHandle = nullptr;
};

And the .cpp:

// Copyright 2024 David Simoes - All rights reserved.

#include "EosSingleton.h"

#include <eos_sdk.h>
#include <eos_auth.h>
#include <eos_connect.h>
#include <eos_achievements.h>

EosSingleton::EosSingleton()
{
    InitSdk();

    CreatePlatform();
    if (PlatformHandle == nullptr)
    {
        return;
    }

    FString token_arg;
    if (!FParse::Value(FCommandLine::Get(), TEXT("AUTH_PASSWORD="), token_arg))
    {
        UE_LOG(LogTemp, Log, TEXT("############### AUTH_PASSWORD not found."));
        return;
    }
    UE_LOG(LogTemp, Log, TEXT("############### AUTH_PASSWORD = %s"), *token_arg);

    AchievementsHandle = EOS_Platform_GetAchievementsInterface(PlatformHandle);
    AuthHandle = EOS_Platform_GetAuthInterface(PlatformHandle);
    ConnectionHandle = EOS_Platform_GetConnectInterface(PlatformHandle);

    AuthLogin(TCHAR_TO_UTF8(*token_arg));

    UE_LOG(LogTemp, Log, TEXT("############### DONE"));
}

EosSingleton::~EosSingleton()
{
}

void EosSingleton::tick()
{
    if (PlatformHandle == nullptr)
    {
        return;
    }
    EOS_Platform_Tick(PlatformHandle);
}

void EosSingleton::InitSdk()
{
    EOS_InitializeOptions SDKOptions;
    SDKOptions.ApiVersion = EOS_INITIALIZE_API_LATEST;
    SDKOptions.AllocateMemoryFunction = NULL;
    SDKOptions.ReallocateMemoryFunction = NULL;
    SDKOptions.ReleaseMemoryFunction = NULL;
    SDKOptions.ProductName = "Chrono";
    SDKOptions.ProductVersion = "1.1";
    SDKOptions.Reserved = NULL;
    SDKOptions.SystemInitializeOptions = NULL;
    SDKOptions.OverrideThreadAffinity = NULL;

    EOS_EResult Result = EOS_Initialize(&SDKOptions);
    if (Result != EOS_EResult::EOS_Success && Result != EOS_EResult::EOS_AlreadyConfigured)
    {
        UE_LOG(LogTemp, Log, TEXT("############### Fatal Error - EOS_Initialize failed. %d"), static_cast<int>(Result));
    }
}

void EosSingleton::CreatePlatform()
{
    FString SandboxId;
    if (!FParse::Value(FCommandLine::Get(), TEXT("epicsandboxid="), SandboxId))
    {
        UE_LOG(LogTemp, Log, TEXT("############### epicsandboxid not found."));
        return;
    }
    FString DeploymentId;
    if (!FParse::Value(FCommandLine::Get(), TEXT("epicdeploymentid="), DeploymentId))
    {
        UE_LOG(LogTemp, Log, TEXT("############### epicdeploymentid not found."));
        return;
    }

    EOS_Platform_Options PlatformOptions = {};
    PlatformOptions.ApiVersion = EOS_PLATFORM_OPTIONS_API_LATEST;
    PlatformOptions.Reserved = NULL;
    PlatformOptions.ProductId = "..."; // TODO
    PlatformOptions.SandboxId = TCHAR_TO_UTF8(*SandboxId);
    PlatformOptions.ClientCredentials.ClientId = "..."; // TODO
    PlatformOptions.ClientCredentials.ClientSecret = "..."; // TODO
    PlatformOptions.bIsServer = false;
    PlatformOptions.EncryptionKey = "..."; // TODO
    PlatformOptions.DeploymentId = TCHAR_TO_UTF8(*DeploymentId);
    PlatformOptions.Flags = 0;
    // CacheDirectory
    PlatformOptions.TickBudgetInMilliseconds = 0;
    PlatformOptions.RTCOptions = NULL;
    PlatformOptions.IntegratedPlatformOptionsContainerHandle = NULL;

    // Create the platform handle.
    PlatformHandle = EOS_Platform_Create(&PlatformOptions);

    if (PlatformHandle == nullptr)
    {
        UE_LOG(LogTemp, Warning, TEXT("############### Failed to create EOS platform."));
    }
}

void EosSingleton::AuthLogin(char *token_arg)
{
    EOS_Auth_Credentials credentials;
    credentials.ApiVersion = EOS_AUTH_CREDENTIALS_API_LATEST;
    credentials.Id = NULL;
    credentials.Token = token_arg;
    credentials.Type = EOS_ELoginCredentialType::EOS_LCT_ExchangeCode;

    EOS_Auth_LoginOptions login_options;
    login_options.ApiVersion = EOS_AUTH_LOGIN_API_LATEST;
    login_options.Credentials = &credentials;
    login_options.ScopeFlags = EOS_EAuthScopeFlags::EOS_AS_BasicProfile;
    // login_options.LoginFlags = 0;

    if (AuthHandle == NULL)
    {
        UE_LOG(LogTemp, Warning, TEXT("failed requesting AuthInterface"));
        return;
    }

    EOS_Auth_Login(AuthHandle, &login_options, nullptr, [](const EOS_Auth_LoginCallbackInfo *Data)
                {
    if (Data->ResultCode == EOS_EResult::EOS_Success)
    {
        // Login successful
        UE_LOG(LogTemp, Log, TEXT("############### EOS_Auth_Login success"));      
        EosSingleton::getInstance().ConnectInterface(Data->LocalUserId);
    }
    else
    {
        // Handle login error
        UE_LOG(LogTemp, Warning, TEXT("############### Fatal Error - EOS_Auth_Login failed."));
    } });
}

void EosSingleton::ConnectInterface(EOS_EpicAccountId account_id)
{
    // https://dev.epicgames.com/docs/game-services/eos-connect-interface#user-authentication-refresh-notification
    EOS_Auth_CopyIdTokenOptions auth_options;
    auth_options.ApiVersion = EOS_AUTH_COPYIDTOKEN_API_LATEST;
    auth_options.AccountId = account_id;
    EOS_Auth_IdToken *id_token = new EOS_Auth_IdToken();
    EOS_EResult result = EOS_Auth_CopyIdToken(AuthHandle, &auth_options, &id_token);
    if (result != EOS_EResult::EOS_Success)
    {
        UE_LOG(LogTemp, Log, TEXT("############### Fatal Error - EOS_Auth_CopyIdToken failed. %d"), static_cast<int>(result));
        return;
    }
    ConnectLogin(id_token->JsonWebToken);
}

void EosSingleton::ConnectLogin(const char *token_arg)
{
    EOS_Connect_Credentials credentials;
    credentials.ApiVersion = EOS_CONNECT_CREDENTIALS_API_LATEST;
    credentials.Token = token_arg;
    credentials.Type = EOS_EExternalCredentialType::EOS_ECT_EPIC_ID_TOKEN;

    EOS_Connect_LoginOptions login_options;
    login_options.ApiVersion = EOS_CONNECT_LOGIN_API_LATEST;
    login_options.Credentials = &credentials;
    login_options.UserLoginInfo = NULL;

    EOS_Connect_Login(ConnectionHandle, &login_options, nullptr, [](const EOS_Connect_LoginCallbackInfo *Data)
                    {
    if (Data->ResultCode == EOS_EResult::EOS_Success)
    {
        // Login successful
        UE_LOG(LogTemp, Log, TEXT("############### EOS_Connect_Login good."));
        EosSingleton::getInstance().LocalProductUserId = Data->LocalUserId;
    }
    else if (Data->ResultCode == EOS_EResult::EOS_InvalidUser)
    {
        UE_LOG(LogTemp, Log, TEXT("############### EOS_Connect_Login invalid user."));
        EosSingleton::getInstance().CreateUser(Data->ContinuanceToken);
    }
    else 
    {
        // Handle login error
        UE_LOG(LogTemp, Warning, TEXT("############### Fatal Error - EOS_Connect_Login failed. %d"), static_cast<int>(Data->ResultCode));
    } });
}

void EosSingleton::CreateUser(const EOS_ContinuanceToken &ContinuanceToken)
{
    EOS_Connect_CreateUserOptions options;
    options.ApiVersion = EOS_CONNECT_CREATEUSER_API_LATEST;
    options.ContinuanceToken = ContinuanceToken;

    EOS_Connect_CreateUser(ConnectionHandle, &options, nullptr, [](const EOS_Connect_CreateUserCallbackInfo *Data)
                        {
    if (Data->ResultCode == EOS_EResult::EOS_Success)
    {
        UE_LOG(LogTemp, Log, TEXT("############### EOS_Connect_CreateUser good."));
        EosSingleton::getInstance().LocalProductUserId = Data->LocalUserId;
    }
    else 
    {
        UE_LOG(LogTemp, Warning, TEXT("############### Fatal Error - EOS_Connect_CreateUser failed. %d"), static_cast<int>(Data->ResultCode));
    } });
}

void EosSingleton::UnlockAchievements(const char **AchievementIds, uint32_t AchievementsCount)
{
    UE_LOG(LogTemp, Log, TEXT("############### UnlockAchievements"));
    if (LocalProductUserId == NULL)
    {
        UE_LOG(LogTemp, Log, TEXT("############### UnlockAchievements but unknown LocalProductUserId."));
        return;
    }

    EOS_Achievements_UnlockAchievementsOptions UnlockAchievementOptions = {};
    UnlockAchievementOptions.ApiVersion = EOS_ACHIEVEMENTS_UNLOCKACHIEVEMENTS_API_LATEST;
    UnlockAchievementOptions.UserId = LocalProductUserId;
    UnlockAchievementOptions.AchievementIds = AchievementIds;
    UnlockAchievementOptions.AchievementsCount = AchievementsCount;

    EOS_Achievements_UnlockAchievements(AchievementsHandle, &UnlockAchievementOptions, nullptr, [](const EOS_Achievements_OnUnlockAchievementsCompleteCallbackInfo *Data)
                                        {
    if (Data->ResultCode == EOS_EResult::EOS_Success)
    {
        UE_LOG(LogTemp, Log, TEXT("############### EOS_Achievements_UnlockAchievements good."));
    }
    else if (EOS_EResult_IsOperationComplete(Data->ResultCode) == EOS_FALSE)
    {
        // If the code gets here, the operation is retrying, meaning it is not yet complete.
        UE_LOG(LogTemp, Log, TEXT("############### EOS_Achievements_UnlockAchievements false, retrying."));
    }
    else
    {
        UE_LOG(LogProcess, Error, TEXT("############### Error unlocking achievement! %d"), static_cast<int>(Data->ResultCode));
    } });
}

In my gamemode, I have something like this:

virtual void Tick(float DeltaTime) override
{
	EosSingleton::getInstance().tick();
}

UFUNCTION(BlueprintCallable, Category = Achievements)
void TriggerSomeAchievement(FString achiev_id)
{
	const char *CharPtr = TCHAR_TO_UTF8(*achiev_id);
	EosSingleton::getInstance().UnlockAchievements(&CharPtr, 1);
}