// Copyright 2020-2024 CesiumGS, Inc. and Contributors #include "CesiumIonTokenTroubleshooting.h" #include "Cesium3DTileset.h" #include "CesiumCommon.h" #include "CesiumEditor.h" #include "CesiumIonClient/Connection.h" #include "CesiumIonRasterOverlay.h" #include "CesiumIonServerDisplay.h" #include "CesiumRuntimeSettings.h" #include "CesiumUtility/Uri.h" #include "EditorStyleSet.h" #include "LevelEditor.h" #include "ScopedTransaction.h" #include "SelectCesiumIonToken.h" #include "Widgets/Images/SImage.h" #include "Widgets/Images/SThrobber.h" #include "Widgets/Input/SButton.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SHeader.h" #include "Widgets/Text/STextBlock.h" using namespace CesiumIonClient; /*static*/ std::vector CesiumIonTokenTroubleshooting::_existingPanels{}; /*static*/ void CesiumIonTokenTroubleshooting::Open( CesiumIonObject ionObject, bool triggeredByError) { auto panelMatch = [ionObject](const ExistingPanel& panel) { return panel.pObject == ionObject; }; // If a panel is already open for this object, close it. auto it = std::find_if( CesiumIonTokenTroubleshooting::_existingPanels.begin(), CesiumIonTokenTroubleshooting::_existingPanels.end(), panelMatch); if (it != CesiumIonTokenTroubleshooting::_existingPanels.end()) { TSharedRef pPanel = it->pPanel; CesiumIonTokenTroubleshooting::_existingPanels.erase(it); FSlateApplication::Get().RequestDestroyWindow(pPanel); } // If this is a tileset, close any already-open panels associated with its // overlays. Overlays won't appear until the tileset is working anyway. TWeakObjectPtr* ppTileset = swl::get_if>(&ionObject); if (ppTileset && ppTileset->IsValid()) { TArray rasterOverlays; (*ppTileset)->GetComponents(rasterOverlays); for (UCesiumRasterOverlay* pOverlay : rasterOverlays) { auto rasterIt = std::find_if( CesiumIonTokenTroubleshooting::_existingPanels.begin(), CesiumIonTokenTroubleshooting::_existingPanels.end(), [pOverlay](const ExistingPanel& candidate) { return candidate.pObject == CesiumIonObject(pOverlay); }); if (rasterIt != CesiumIonTokenTroubleshooting::_existingPanels.end()) { TSharedRef pPanel = rasterIt->pPanel; CesiumIonTokenTroubleshooting::_existingPanels.erase(rasterIt); FSlateApplication::Get().RequestDestroyWindow(pPanel); } } } // If this is a raster overlay and this panel is already open for its attached // tileset, don't open the panel for the overlay for the same reason as above. TWeakObjectPtr* ppRasterOverlay = swl::get_if>(&ionObject); if (ppRasterOverlay && ppRasterOverlay->IsValid()) { ACesium3DTileset* pOwner = Cast((*ppRasterOverlay)->GetOwner()); if (pOwner) { auto tilesetIt = std::find_if( CesiumIonTokenTroubleshooting::_existingPanels.begin(), CesiumIonTokenTroubleshooting::_existingPanels.end(), [pOwner](const ExistingPanel& candidate) { return candidate.pObject == CesiumIonObject(pOwner); }); if (tilesetIt != CesiumIonTokenTroubleshooting::_existingPanels.end()) { return; } } } // Open the panel TSharedRef Troubleshooting = SNew(CesiumIonTokenTroubleshooting) .IonObject(ionObject) .TriggeredByError(triggeredByError); Troubleshooting->GetOnWindowClosedEvent().AddLambda( [panelMatch](const TSharedRef& pWindow) { auto it = std::find_if( CesiumIonTokenTroubleshooting::_existingPanels.begin(), CesiumIonTokenTroubleshooting::_existingPanels.end(), panelMatch); if (it != CesiumIonTokenTroubleshooting::_existingPanels.end()) { CesiumIonTokenTroubleshooting::_existingPanels.erase(it); } }); FSlateApplication::Get().AddWindow(Troubleshooting); CesiumIonTokenTroubleshooting::_existingPanels.emplace_back( ExistingPanel{ionObject, Troubleshooting}); } namespace { TSharedRef addTokenCheck(const FString& label, std::optional& state) { return SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().Padding( 3.0f, 0.0f, 3.0f, 0.0f)[SNew(SThrobber) .Visibility_Lambda([&state]() { return state.has_value() ? EVisibility::Collapsed : EVisibility::Visible; }) .NumPieces(1) .Animate(SThrobber::All)] + SHorizontalBox::Slot().AutoWidth().Padding( 5.0f, 0.0f, 5.0f, 3.0f)[SNew(SImage) .Visibility_Lambda([&state]() { return state.has_value() ? EVisibility::Visible : EVisibility::Collapsed; }) .Image_Lambda([&state]() { return state.has_value() && *state ? FCesiumEditorModule::GetStyle()->GetBrush( TEXT("Cesium.Common.GreenTick")) : FCesiumEditorModule::GetStyle()->GetBrush( TEXT("Cesium.Common.RedX")); })] + SHorizontalBox::Slot() .AutoWidth()[SNew(STextBlock).Text(FText::FromString(label))]; } bool isNull(const CesiumIonObject& o) { return swl::visit([](auto p) { return p == nullptr; }, o); } FString getLabel(const CesiumIonObject& o) { struct Operation { FString operator()(TWeakObjectPtr pTileset) { return pTileset.IsValid() ? pTileset->GetActorLabel() : TEXT("Unknown"); } FString operator()(TWeakObjectPtr pRasterOverlay) { return pRasterOverlay.IsValid() ? pRasterOverlay->GetName() : TEXT("Unknown"); } }; return swl::visit(Operation(), o); } FString getName(const CesiumIonObject& o) { return swl::visit([](auto p) { return p->GetName(); }, o); } int64 getIonAssetID(const CesiumIonObject& o) { struct Operation { int64 operator()(TWeakObjectPtr pTileset) { if (!pTileset.IsValid()) return 0; if (pTileset->GetTilesetSource() != ETilesetSource::FromCesiumIon) { return 0; } else { return pTileset->GetIonAssetID(); } } int64 operator()(TWeakObjectPtr pRasterOverlay) { if (!pRasterOverlay.IsValid()) return 0; UCesiumIonRasterOverlay* pIon = Cast(pRasterOverlay); if (!pIon) { return 0; } else { return pIon->IonAssetID; } } }; return swl::visit(Operation(), o); } FString getIonAccessToken(const CesiumIonObject& o) { struct Operation { FString operator()(TWeakObjectPtr pTileset) { if (!pTileset.IsValid()) return FString(); if (pTileset->GetTilesetSource() != ETilesetSource::FromCesiumIon) { return FString(); } else { return pTileset->GetIonAccessToken(); } } FString operator()(TWeakObjectPtr pRasterOverlay) { if (!pRasterOverlay.IsValid()) return FString(); UCesiumIonRasterOverlay* pIon = Cast(pRasterOverlay); if (!pIon) { return FString(); } else { return pIon->IonAccessToken; } } }; return swl::visit(Operation(), o); } void setIonAccessToken(const CesiumIonObject& o, const FString& newToken) { struct Operation { const FString& newToken; void operator()(TWeakObjectPtr pTileset) { if (!pTileset.IsValid()) return; if (pTileset->GetIonAccessToken() != newToken) { pTileset->Modify(); pTileset->SetIonAccessToken(newToken); } else { pTileset->RefreshTileset(); } } void operator()(TWeakObjectPtr pRasterOverlay) { if (!pRasterOverlay.IsValid()) return; UCesiumIonRasterOverlay* pIon = Cast(pRasterOverlay); if (!pIon) { return; } if (pIon->IonAccessToken != newToken) { pIon->Modify(); pIon->IonAccessToken = newToken; } pIon->Refresh(); } }; return swl::visit(Operation{newToken}, o); } FString getObjectType(const CesiumIonObject& o) { struct Operation { FString operator()(TWeakObjectPtr pTileset) { return TEXT("Tileset"); } FString operator()(TWeakObjectPtr pRasterOverlay) { return TEXT("Raster Overlay"); } }; return swl::visit(Operation(), o); } UObject* asUObject(const CesiumIonObject& o) { return swl::visit( [](auto p) -> UObject* { return p.IsValid() ? p.Get() : nullptr; }, o); } bool isUsingCesiumIon(const CesiumIonObject& o) { struct Operation { bool operator()(TWeakObjectPtr pTileset) { return pTileset.IsValid() && pTileset->GetTilesetSource() == ETilesetSource::FromCesiumIon; } bool operator()(TWeakObjectPtr pRasterOverlay) { if (!pRasterOverlay.IsValid()) return false; UCesiumIonRasterOverlay* pIon = Cast(pRasterOverlay); return pIon != nullptr; } }; return swl::visit(Operation(), o); } UCesiumIonServer* getCesiumIonServer(const CesiumIonObject& o) { struct Operation { UCesiumIonServer* operator()(TWeakObjectPtr pTileset) noexcept { return pTileset.IsValid() ? pTileset->GetCesiumIonServer() : nullptr; } UCesiumIonServer* operator()(TWeakObjectPtr pRasterOverlay) noexcept { if (!pRasterOverlay.IsValid()) return nullptr; const UCesiumIonRasterOverlay* pIon = Cast(pRasterOverlay); return pIon ? pIon->CesiumIonServer : nullptr; } }; UCesiumIonServer* pServer = swl::visit(Operation{}, o); if (!IsValid(pServer)) { pServer = UCesiumIonServer::GetDefaultServer(); } return pServer; } CesiumIonSession& getSession(const CesiumIonObject& o) { return *FCesiumEditorModule::serverManager().GetSession( getCesiumIonServer(o)); } } // namespace void CesiumIonTokenTroubleshooting::Construct(const FArguments& InArgs) { TSharedRef pMainVerticalBox = SNew(SVerticalBox); CesiumIonObject pIonObject = InArgs._IonObject; if (isNull(pIonObject)) { return; } if (!isUsingCesiumIon(pIonObject)) { SWindow::Construct( SWindow::FArguments() .Title(FText::FromString(FString::Format( TEXT("{0}: Cesium ion Token Troubleshooting"), {*getLabel(pIonObject)}))) .AutoCenter(EAutoCenter::PreferredWorkArea) .SizingRule(ESizingRule::UserSized) .ClientSize(FVector2D(800, 600)) [SNew(SBorder) .Visibility(EVisibility::Visible) .BorderImage(FAppStyle::GetBrush("Menu.Background")) .Padding(FMargin(10.0f, 20.0f, 10.0f, 20.0f)) [SNew(STextBlock) .AutoWrapText(true) .Text(FText::FromString(TEXT( "This object is not configured to connect to Cesium ion.")))]]); return; } this->_pIonObject = pIonObject; if (InArgs._TriggeredByError) { FString label = getLabel(pIonObject); FString name = getName(pIonObject); FString descriptor = label == name ? FString::Format(TEXT("\"{0}\""), {name}) : FString::Format(TEXT("\"{0}\" ({1})"), {label, name}); FString preamble = FString::Format( TEXT( "{0} {1} tried to access Cesium ion for asset ID {2}, but it didn't work, probably due to a problem with the access token. This panel will help you fix it!"), {getObjectType(pIonObject), *descriptor, getIonAssetID(pIonObject)}); pMainVerticalBox->AddSlot().AutoHeight() [SNew(STextBlock).AutoWrapText(true).Text(FText::FromString(preamble))]; } pMainVerticalBox->AddSlot().AutoHeight().Padding( 5.0f)[SNew(CesiumIonServerDisplay) .Server(getCesiumIonServer(pIonObject))]; TSharedRef pDiagnosticColumns = SNew(SHorizontalBox); if (!getIonAccessToken(pIonObject).IsEmpty()) { this->_assetTokenState.name = FString::Format( TEXT("This {0}'s Access Token"), {getObjectType(pIonObject)}); this->_assetTokenState.token = getIonAccessToken(pIonObject); pDiagnosticColumns->AddSlot() .Padding(5.0f, 20.0f, 5.0f, 5.0f) .VAlign(EVerticalAlignment::VAlign_Top) .AutoWidth() .FillWidth( 0.5f)[this->createTokenPanel(pIonObject, this->_assetTokenState)]; } this->_projectDefaultTokenState.name = TEXT("Project Default Access Token"); this->_projectDefaultTokenState.token = getCesiumIonServer(pIonObject)->DefaultIonAccessToken; pDiagnosticColumns->AddSlot() .Padding(5.0f, 20.0f, 5.0f, 5.0f) .VAlign(EVerticalAlignment::VAlign_Top) .AutoWidth() .FillWidth(0.5f) [this->createTokenPanel(pIonObject, this->_projectDefaultTokenState)]; if (getSession(this->_pIonObject).isConnected()) { // Don't let this panel be destroyed while the async operations below are in // progress. TSharedRef pPanel = StaticCastSharedRef(this->AsShared()); getSession(this->_pIonObject) .getConnection() ->asset(getIonAssetID(pIonObject)) .thenInMainThread([pPanel](Response&& asset) { pPanel->_assetExistsInUserAccount = asset.value.has_value(); }); // Start a new row if we have more than two columns. if (pDiagnosticColumns->NumSlots() >= 2) { pMainVerticalBox->AddSlot().AutoHeight()[pDiagnosticColumns]; pDiagnosticColumns = SNew(SHorizontalBox); } pDiagnosticColumns->AddSlot() .Padding(5.0f, 20.0f, 5.0f, 5.0f) .VAlign(EVerticalAlignment::VAlign_Top) .AutoWidth() .FillWidth(0.5f)[this->createDiagnosticPanel( TEXT("Asset"), {addTokenCheck( TEXT("Asset ID exists in your user account"), this->_assetExistsInUserAccount)})]; } pMainVerticalBox->AddSlot().AutoHeight()[pDiagnosticColumns]; this->addRemedyButton( pMainVerticalBox, TEXT("Connect to Cesium ion"), &CesiumIonTokenTroubleshooting::canConnectToCesiumIon, &CesiumIonTokenTroubleshooting::connectToCesiumIon); this->addRemedyButton( pMainVerticalBox, FString::Format( TEXT("Use the project default token for this {0}"), {getObjectType(pIonObject)}), &CesiumIonTokenTroubleshooting::canUseProjectDefaultToken, &CesiumIonTokenTroubleshooting::useProjectDefaultToken); this->addRemedyButton( pMainVerticalBox, FString::Format( TEXT("Authorize the {0}'s token to access this asset"), {getObjectType(pIonObject)}), &CesiumIonTokenTroubleshooting::canAuthorizeAssetToken, &CesiumIonTokenTroubleshooting::authorizeAssetToken); this->addRemedyButton( pMainVerticalBox, TEXT("Authorize the project default token to access this asset"), &CesiumIonTokenTroubleshooting::canAuthorizeProjectDefaultToken, &CesiumIonTokenTroubleshooting::authorizeProjectDefaultToken); this->addRemedyButton( pMainVerticalBox, TEXT("Select or create a new project default token"), &CesiumIonTokenTroubleshooting::canSelectNewProjectDefaultToken, &CesiumIonTokenTroubleshooting::selectNewProjectDefaultToken); pMainVerticalBox->AddSlot().AutoHeight().Padding(0.0f, 20.0f, 0.0f, 0.0f) [SNew(STextBlock) .Visibility_Lambda([this]() { return (this->_assetTokenState.token.IsEmpty() || this->_assetTokenState.allowsAccessToAsset == false) && (this->_projectDefaultTokenState.token.IsEmpty() || this->_projectDefaultTokenState .allowsAccessToAsset == false) && this->_assetExistsInUserAccount == false ? EVisibility::Visible : EVisibility::Collapsed; }) .AutoWrapText(true) .Text(FText::FromString(FString::Format( TEXT( "No automatic remedies are possible for Asset ID {0}, because:\n" " - The current token does not authorize access to the specified asset ID, and\n" " - The asset ID does not exist in your Cesium ion account.\n" "\n" "Please click the button below to open Cesium ion and check:\n" " - The {1}'s \"Ion Asset ID\" property is correct.\n" " - If the asset is from the \"Asset Depot\", verify that it has been added to \"My Assets\"."), {getIonAssetID(pIonObject), getObjectType(pIonObject)})))]; this->addRemedyButton( pMainVerticalBox, TEXT("Open Cesium ion on the Web"), &CesiumIonTokenTroubleshooting::canOpenCesiumIon, &CesiumIonTokenTroubleshooting::openCesiumIon); SWindow::Construct( SWindow::FArguments() .Title(FText::FromString(FString::Format( TEXT("{0}: Cesium ion Token Troubleshooting"), {*getLabel(pIonObject)}))) .AutoCenter(EAutoCenter::PreferredWorkArea) .SizingRule(ESizingRule::UserSized) .ClientSize(FVector2D(800, 600)) [SNew(SBorder) .Visibility(EVisibility::Visible) .BorderImage(FAppStyle::GetBrush("Menu.Background")) .Padding( FMargin(10.0f, 10.0f, 10.0f, 10.0f))[pMainVerticalBox]]); } TSharedRef CesiumIonTokenTroubleshooting::createDiagnosticPanel( const FString& name, const TArray>& diagnostics) { TSharedRef pRows = SNew(SVerticalBox) + SVerticalBox::Slot().Padding( 0.0f, 5.0f, 0.0f, 5.0f)[SNew(SHeader).Content() [SNew(STextBlock) .TextStyle(FCesiumEditorModule::GetStyle(), "Heading") .Text(FText::FromString(name))]]; for (const TSharedRef& diagnostic : diagnostics) { pRows->AddSlot().Padding(0.0f, 5.0f, 0.0f, 5.0f)[diagnostic]; } return pRows; } TSharedRef CesiumIonTokenTroubleshooting::createTokenPanel( const CesiumIonObject& pIonObject, TokenState& state) { CesiumIonSession& ionSession = getSession(pIonObject); int64 assetID = getIonAssetID(pIonObject); auto pConnection = std::make_shared( ionSession.getAsyncSystem(), ionSession.getAssetAccessor(), TCHAR_TO_UTF8(*state.token), ionSession.getAppData(), TCHAR_TO_UTF8(*getCesiumIonServer(pIonObject)->ApiUrl)); // Don't let this panel be destroyed while the async operations below are in // progress. TSharedRef pPanel = StaticCastSharedRef(this->AsShared()); pConnection->me() .thenInMainThread( [pPanel, pConnection, assetID, &state](Response&& profile) { state.isValid = profile.value.has_value(); if (pPanel->IsVisible()) { return pConnection->asset(assetID); } else { return pConnection->getAsyncSystem().createResolvedFuture( Response{}); } }) .thenInMainThread([pPanel, pConnection, &state](Response&& asset) { state.allowsAccessToAsset = asset.value.has_value(); if (pPanel->IsVisible()) { // Query the tokens using the user's connection (_not_ the token // connection created above). CesiumIonSession& ionSession = getSession(pPanel->_pIonObject); ionSession.resume(); const std::optional& userConnection = ionSession.getConnection(); if (!userConnection) { Response result{}; return ionSession.getAsyncSystem().createResolvedFuture( std::move(result)); } return userConnection->tokens(); } else { return pConnection->getAsyncSystem().createResolvedFuture( Response{}); } }) .thenInMainThread( [pPanel, pConnection, &state](Response&& tokens) { state.associatedWithUserAccount = false; if (tokens.value.has_value()) { auto it = std::find_if( tokens.value->items.begin(), tokens.value->items.end(), [&pConnection](const Token& token) { return token.token == pConnection->getAccessToken(); }); state.associatedWithUserAccount = it != tokens.value->items.end(); } }); return this->createDiagnosticPanel( state.name, {addTokenCheck(TEXT("Is a valid Cesium ion token"), state.isValid), addTokenCheck( TEXT("Allows access to this asset"), state.allowsAccessToAsset), addTokenCheck( TEXT("Is associated with your user account"), state.associatedWithUserAccount)}); } void CesiumIonTokenTroubleshooting::addRemedyButton( const TSharedRef& pParent, const FString& name, bool (CesiumIonTokenTroubleshooting::*isAvailableCallback)() const, void (CesiumIonTokenTroubleshooting::*clickCallback)()) { pParent->AddSlot().AutoHeight().Padding( 0.0f, 20.0f, 0.0f, 5.0f)[SNew(SButton) .ButtonStyle(FCesiumEditorModule::GetStyle(), "CesiumButton") .TextStyle(FCesiumEditorModule::GetStyle(), "CesiumButtonText") .OnClicked_Lambda([this, clickCallback]() { std::invoke(clickCallback, *this); this->RequestDestroyWindow(); return FReply::Handled(); }) .Text(FText::FromString(name)) .Visibility_Lambda([this, isAvailableCallback]() { return std::invoke(isAvailableCallback, *this) ? EVisibility::Visible : EVisibility::Collapsed; })]; } bool CesiumIonTokenTroubleshooting::canConnectToCesiumIon() const { return !getSession(this->_pIonObject).isConnected(); } void CesiumIonTokenTroubleshooting::connectToCesiumIon() { // Pop up the Cesium panel to show the status. FLevelEditorModule* pLevelEditorModule = FModuleManager::GetModulePtr( FName(TEXT("LevelEditor"))); TSharedPtr pTabManager = pLevelEditorModule ? pLevelEditorModule->GetLevelEditorTabManager() : FGlobalTabmanager::Get(); pTabManager->TryInvokeTab(FTabId(TEXT("Cesium"))); // Pop up a browser window to sign in to ion. getSession(this->_pIonObject).connect(); } bool CesiumIonTokenTroubleshooting::canUseProjectDefaultToken() const { const TokenState& state = this->_projectDefaultTokenState; return !isNull(this->_pIonObject) && !getIonAccessToken(this->_pIonObject).IsEmpty() && state.isValid == true && state.allowsAccessToAsset == true; } void CesiumIonTokenTroubleshooting::useProjectDefaultToken() { if (isNull(this->_pIonObject)) { return; } FScopedTransaction transaction( FText::FromString("Use Project Default Token")); setIonAccessToken(this->_pIonObject, FString()); } bool CesiumIonTokenTroubleshooting::canAuthorizeAssetToken() const { const TokenState& state = this->_assetTokenState; return this->_assetExistsInUserAccount == true && state.isValid == true && state.allowsAccessToAsset == false && state.associatedWithUserAccount == true; } void CesiumIonTokenTroubleshooting::authorizeAssetToken() { if (isNull(this->_pIonObject)) { return; } this->authorizeToken(getIonAccessToken(this->_pIonObject), false); } bool CesiumIonTokenTroubleshooting::canAuthorizeProjectDefaultToken() const { const TokenState& state = this->_projectDefaultTokenState; return this->_assetExistsInUserAccount == true && state.isValid == true && state.allowsAccessToAsset == false && state.associatedWithUserAccount == true; } void CesiumIonTokenTroubleshooting::authorizeProjectDefaultToken() { UCesiumIonServer* pServer = getCesiumIonServer(this->_pIonObject); this->authorizeToken(pServer->DefaultIonAccessToken, true); } bool CesiumIonTokenTroubleshooting::canSelectNewProjectDefaultToken() const { if (this->_assetExistsInUserAccount == false) { return false; } const TokenState& state = this->_projectDefaultTokenState; return getSession(this->_pIonObject).isConnected() && (state.isValid == false || (state.allowsAccessToAsset == false && state.associatedWithUserAccount == false)); } void CesiumIonTokenTroubleshooting::selectNewProjectDefaultToken() { if (isNull(this->_pIonObject)) { return; } CesiumIonSession& session = getSession(this->_pIonObject); const std::optional& maybeConnection = session.getConnection(); if (!session.isConnected() || !maybeConnection) { UE_LOG( LogCesiumEditor, Error, TEXT( "Cannot create a new project default token because you are not signed in to Cesium ion.")); return; } // Don't let this panel be destroyed while the async operations below are in // progress. TSharedRef pPanel = StaticCastSharedRef(this->AsShared()); SelectCesiumIonToken::SelectNewToken(getCesiumIonServer(this->_pIonObject)) .thenInMainThread([pPanel](const std::optional& newToken) { if (!newToken) { return; } pPanel->useProjectDefaultToken(); }); } bool CesiumIonTokenTroubleshooting::canOpenCesiumIon() const { return getSession(this->_pIonObject).isConnected(); } void CesiumIonTokenTroubleshooting::openCesiumIon() { UCesiumIonServer* pServer = getCesiumIonServer(this->_pIonObject); FPlatformProcess::LaunchURL( UTF8_TO_TCHAR(CesiumUtility::Uri::resolve( TCHAR_TO_UTF8(*pServer->ServerUrl), "tokens") .c_str()), NULL, NULL); } void CesiumIonTokenTroubleshooting::authorizeToken( const FString& token, bool removeObjectToken) { if (isNull(this->_pIonObject)) { return; } CesiumIonSession& session = getSession(this->_pIonObject); const std::optional& maybeConnection = session.getConnection(); if (!session.isConnected() || !maybeConnection) { UE_LOG( LogCesiumEditor, Error, TEXT( "Cannot grant a token access to an asset because you are not signed in to Cesium ion.")); return; } TWeakObjectPtr pStillAlive = asUObject(this->_pIonObject); session.findToken(token).thenInMainThread([pStillAlive, removeObjectToken, pIonObject = this->_pIonObject, ionAssetID = getIonAssetID( this->_pIonObject), connection = *maybeConnection]( Response&& response) { if (!pStillAlive.IsValid()) { // UObject has been destroyed return connection.getAsyncSystem().createResolvedFuture(); } if (!response.value) { UE_LOG( LogCesiumEditor, Error, TEXT( "Cannot grant a token access to an asset because the token was not found in the signed-in Cesium ion account.")); return connection.getAsyncSystem().createResolvedFuture(); } if (!response.value->assetIds) { UE_LOG( LogCesiumEditor, Warning, TEXT( "Cannot grant a token access to an asset because the token appears to already have access to all assets.")); return connection.getAsyncSystem().createResolvedFuture(); } auto it = std::find( response.value->assetIds->begin(), response.value->assetIds->end(), ionAssetID); if (it != response.value->assetIds->end()) { UE_LOG( LogCesiumEditor, Warning, TEXT( "Cannot grant a token access to an asset because the token appears to already have access to the asset.")); return connection.getAsyncSystem().createResolvedFuture(); } response.value->assetIds->emplace_back(ionAssetID); return connection .modifyToken( response.value->id, response.value->name, response.value->assetIds, response.value->scopes, response.value->allowedUrls) .thenInMainThread([pIonObject, pStillAlive, removeObjectToken]( Response&& result) { if (result.value) { // Refresh the object now that the token is valid (hopefully). if (pStillAlive.IsValid()) { if (removeObjectToken) { setIonAccessToken(pIonObject, FString()); } else { // Set the token to the same value to force a refresh. setIonAccessToken(pIonObject, getIonAccessToken(pIonObject)); } } } else { UE_LOG( LogCesiumEditor, Error, TEXT( "An error occurred while attempting to modify a token to grant it access to an asset. Please visit https://cesium.com/ion/tokens to modify the token manually.")); } }); }); }